diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/const.go | 9 | ||||
-rw-r--r-- | api/const_string.go | 4 | ||||
-rw-r--r-- | api/external_services.go | 40 | ||||
-rw-r--r-- | api/market_config.go | 76 | ||||
-rw-r--r-- | api/markets.go | 29 | ||||
-rw-r--r-- | api/routes.go | 11 |
6 files changed, 166 insertions, 3 deletions
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" | |||
6 | type Status uint32 | 6 | type Status uint32 |
7 | type ErrorCode uint32 | 7 | type ErrorCode uint32 |
8 | 8 | ||
9 | const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 10 | ||
10 | |||
9 | const ( | 11 | const ( |
10 | OK Status = iota | 12 | OK Status = iota |
11 | NOK | 13 | NOK |
12 | 14 | ||
13 | BadRequest ErrorCode = iota + 1 | 15 | BadRequest ErrorCode = iota + 1 |
14 | EmailExists | 16 | EmailExists |
17 | ExternalServiceTimeout | ||
15 | InternalError | 18 | InternalError |
16 | InvalidCredentials | 19 | InvalidCredentials |
17 | InvalidEmail | 20 | InvalidEmail |
21 | InvalidMarketCredentials | ||
18 | InvalidOtp | 22 | InvalidOtp |
19 | InvalidPassword | 23 | InvalidPassword |
20 | NeedOtpValidation | 24 | NeedOtpValidation |
@@ -31,7 +35,7 @@ func StatusToHttpCode(status Status, code ErrorCode) int { | |||
31 | } | 35 | } |
32 | 36 | ||
33 | switch code { | 37 | switch code { |
34 | case BadRequest, InvalidPassword, InvalidEmail: | 38 | case BadRequest, InvalidPassword, InvalidEmail, InvalidMarketCredentials: |
35 | return http.StatusBadRequest | 39 | return http.StatusBadRequest |
36 | 40 | ||
37 | case InvalidCredentials, InvalidOtp: | 41 | case InvalidCredentials, InvalidOtp: |
@@ -45,6 +49,9 @@ func StatusToHttpCode(status Status, code ErrorCode) int { | |||
45 | 49 | ||
46 | case NotFound: | 50 | case NotFound: |
47 | return http.StatusNotFound | 51 | return http.StatusNotFound |
52 | |||
53 | case ExternalServiceTimeout: | ||
54 | return http.StatusGatewayTimeout | ||
48 | } | 55 | } |
49 | 56 | ||
50 | return http.StatusInternalServerError | 57 | 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 { | |||
15 | return _Status_name[_Status_index[i]:_Status_index[i+1]] | 15 | return _Status_name[_Status_index[i]:_Status_index[i+1]] |
16 | } | 16 | } |
17 | 17 | ||
18 | const _ErrorCode_name = "BadRequestEmailExistsInternalErrorInvalidCredentialsInvalidEmailInvalidOtpInvalidPasswordNeedOtpValidationNotAuthorizedNotFoundOtpAlreadySetupOtpNotSetupUserNotConfirmed" | 18 | const _ErrorCode_name = "BadRequestEmailExistsExternalServiceTimeoutInternalErrorInvalidCredentialsInvalidEmailInvalidMarketCredentialsInvalidOtpInvalidPasswordNeedOtpValidationNotAuthorizedNotFoundOtpAlreadySetupOtpNotSetupUserNotConfirmed" |
19 | 19 | ||
20 | var _ErrorCode_index = [...]uint8{0, 10, 21, 34, 52, 64, 74, 89, 106, 119, 127, 142, 153, 169} | 20 | var _ErrorCode_index = [...]uint8{0, 10, 21, 43, 56, 74, 86, 110, 120, 135, 152, 165, 173, 188, 199, 215} |
21 | 21 | ||
22 | func (i ErrorCode) String() string { | 22 | func (i ErrorCode) String() string { |
23 | i -= 3 | 23 | 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 @@ | |||
1 | package api | ||
2 | |||
3 | import ( | ||
4 | "context" | ||
5 | "fmt" | ||
6 | "time" | ||
7 | ) | ||
8 | |||
9 | // Use this to call external services. It will handle timeout and request cancellation gracefully. | ||
10 | func CallExternalService(tag string, timeout time.Duration, routine func() *Error) *Error { | ||
11 | routineDone := make(chan *Error) | ||
12 | |||
13 | go func() { | ||
14 | routineDone <- routine() | ||
15 | }() | ||
16 | |||
17 | ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||
18 | defer cancel() | ||
19 | |||
20 | select { | ||
21 | case err := <-routineDone: | ||
22 | return err | ||
23 | case <-ctx.Done(): | ||
24 | return &Error{ExternalServiceTimeout, "external service timeout", fmt.Errorf("'%v' routine timeouted", tag)} | ||
25 | } | ||
26 | } | ||
27 | |||
28 | var ErrorChan chan error | ||
29 | |||
30 | func ErrorMonitoring() { | ||
31 | for { | ||
32 | err := <-ErrorChan | ||
33 | log.Errorf("error: %v", err) | ||
34 | } | ||
35 | } | ||
36 | |||
37 | func init() { | ||
38 | ErrorChan = make(chan error) | ||
39 | go ErrorMonitoring() | ||
40 | } | ||
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 | |||
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "fmt" | 4 | "fmt" |
5 | "strings" | ||
6 | "time" | ||
5 | 7 | ||
8 | "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets" | ||
9 | |||
10 | "github.com/jloup/utils" | ||
11 | "github.com/shopspring/decimal" | ||
6 | "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" | 12 | "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" |
7 | ) | 13 | ) |
8 | 14 | ||
@@ -42,6 +48,73 @@ func (q MarketConfigQuery) Run() (interface{}, *Error) { | |||
42 | return config.Config, nil | 48 | return config.Config, nil |
43 | } | 49 | } |
44 | 50 | ||
51 | type MarketBalanceQuery struct { | ||
52 | In struct { | ||
53 | User db.User | ||
54 | Market string | ||
55 | Currency string | ||
56 | } | ||
57 | } | ||
58 | |||
59 | func (q MarketBalanceQuery) ValidateParams() *Error { | ||
60 | if q.In.Market != "poloniex" { | ||
61 | return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)} | ||
62 | } | ||
63 | |||
64 | // TODO: we should request market for available currencies. | ||
65 | if q.In.Currency != "BTC" && q.In.Currency != "USDT" && q.In.Currency != "ETH" { | ||
66 | return &Error{BadRequest, "invalid currency, accept [BTC, USDT, ETH]", fmt.Errorf("'%v' is not a valid currency", q.In.Currency)} | ||
67 | } | ||
68 | |||
69 | return nil | ||
70 | } | ||
71 | |||
72 | func (q MarketBalanceQuery) Run() (interface{}, *Error) { | ||
73 | config, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market) | ||
74 | if err != nil { | ||
75 | return nil, NewInternalError(err) | ||
76 | } | ||
77 | |||
78 | if config.Config["key"] == "" || config.Config["secret"] == "" { | ||
79 | return nil, &Error{BadRequest, "your credentials for this market are not setup", fmt.Errorf("'%v' credentials are not setup", q.In.Market)} | ||
80 | } | ||
81 | |||
82 | result := struct { | ||
83 | Value decimal.Decimal `json:"value"` | ||
84 | ValueCurrency string `json:"valueCurrency"` | ||
85 | Balance map[string]decimal.Decimal `json:"balance"` | ||
86 | }{} | ||
87 | |||
88 | resultErr := CallExternalService(fmt.Sprintf("'%s' GetBalanceValue", q.In.Market), EXTERNAL_SERVICE_TIMEOUT_SECONDS*time.Second, func() *Error { | ||
89 | balance, err := Poloniex.GetBalance(config.Config["key"], config.Config["secret"]) | ||
90 | |||
91 | if utils.ErrIs(err, markets.InvalidCredentials) { | ||
92 | return &Error{InvalidMarketCredentials, "wrong market credentials", fmt.Errorf("wrong '%v' market credentials", q.In.Market)} | ||
93 | } | ||
94 | |||
95 | if err != nil { | ||
96 | return NewInternalError(err) | ||
97 | } | ||
98 | |||
99 | value, err := Poloniex.ComputeAccountBalanceValue(balance, q.In.Currency) | ||
100 | if err != nil { | ||
101 | return NewInternalError(err) | ||
102 | } | ||
103 | |||
104 | result.Balance = balance | ||
105 | result.ValueCurrency = q.In.Currency | ||
106 | result.Value = value.Round(8) | ||
107 | |||
108 | return nil | ||
109 | }) | ||
110 | |||
111 | if resultErr != nil { | ||
112 | return nil, resultErr | ||
113 | } | ||
114 | |||
115 | return &result, nil | ||
116 | } | ||
117 | |||
45 | type UpdateMarketConfigQuery struct { | 118 | type UpdateMarketConfigQuery struct { |
46 | In struct { | 119 | In struct { |
47 | User db.User | 120 | User db.User |
@@ -56,6 +129,9 @@ func (q UpdateMarketConfigQuery) ValidateParams() *Error { | |||
56 | return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)} | 129 | return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)} |
57 | } | 130 | } |
58 | 131 | ||
132 | q.In.Secret = strings.TrimSpace(q.In.Secret) | ||
133 | q.In.Key = strings.TrimSpace(q.In.Key) | ||
134 | |||
59 | return nil | 135 | return nil |
60 | } | 136 | } |
61 | 137 | ||
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 @@ | |||
1 | package api | ||
2 | |||
3 | import ( | ||
4 | "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets" | ||
5 | ) | ||
6 | |||
7 | var Poloniex *markets.Poloniex | ||
8 | |||
9 | func OpenMarketsConnection() error { | ||
10 | for { | ||
11 | err := Poloniex.StartTicker() | ||
12 | if err != nil { | ||
13 | return err | ||
14 | } | ||
15 | log.Warn("connection to poloniex stream ended, restarting it...") | ||
16 | } | ||
17 | } | ||
18 | |||
19 | func init() { | ||
20 | Poloniex = markets.NewPoloniex() | ||
21 | |||
22 | // We open markets connections in the background as it can take time. | ||
23 | go func() { | ||
24 | err := OpenMarketsConnection() | ||
25 | if err != nil { | ||
26 | ErrorChan <- err | ||
27 | } | ||
28 | }() | ||
29 | } | ||
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{ | |||
41 | []Route{ | 41 | []Route{ |
42 | {"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"}, | 42 | {"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"}, |
43 | {"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"}, | 43 | {"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"}, |
44 | {"GET", []gin.HandlerFunc{GetMarketBalance}, "/:name/balance"}, | ||
44 | }, | 45 | }, |
45 | }, | 46 | }, |
46 | } | 47 | } |
@@ -111,6 +112,16 @@ func GetMarketConfig(c *gin.Context) { | |||
111 | RunQuery(query, c) | 112 | RunQuery(query, c) |
112 | } | 113 | } |
113 | 114 | ||
115 | func GetMarketBalance(c *gin.Context) { | ||
116 | query := &MarketBalanceQuery{} | ||
117 | |||
118 | query.In.User = GetUser(c) | ||
119 | query.In.Market = c.Param("name") | ||
120 | query.In.Currency = c.Query("currency") | ||
121 | |||
122 | RunQuery(query, c) | ||
123 | } | ||
124 | |||
114 | func UpdateMarketConfig(c *gin.Context) { | 125 | func UpdateMarketConfig(c *gin.Context) { |
115 | query := &UpdateMarketConfigQuery{} | 126 | query := &UpdateMarketConfigQuery{} |
116 | 127 | ||