diff options
-rw-r--r-- | Gopkg.lock | 22 | ||||
-rw-r--r-- | Gopkg.toml | 8 | ||||
-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 | ||||
-rw-r--r-- | cmd/web/Makefile | 19 | ||||
-rw-r--r-- | cmd/web/js/api.js | 11 | ||||
-rw-r--r-- | cmd/web/js/main.jsx | 41 | ||||
-rw-r--r-- | cmd/web/js/otp.jsx | 6 | ||||
-rw-r--r-- | cmd/web/js/poloniex.jsx | 176 | ||||
-rw-r--r-- | cmd/web/js/signin.jsx | 4 | ||||
-rw-r--r-- | cmd/web/js/signup.jsx | 4 | ||||
-rw-r--r-- | cmd/web/static/cryptocoins.css | 1150 | ||||
-rw-r--r-- | cmd/web/static/cryptocoins.ttf | bin | 0 -> 96448 bytes | |||
-rw-r--r-- | cmd/web/static/cryptocoins.woff | bin | 0 -> 58768 bytes | |||
-rw-r--r-- | cmd/web/static/cryptocoins.woff2 | bin | 0 -> 49976 bytes | |||
-rw-r--r-- | cmd/web/static/index.html | 1 | ||||
-rw-r--r-- | cmd/web/static/style.css | 15 | ||||
-rw-r--r-- | markets/poloniex.go | 177 |
22 files changed, 1732 insertions, 71 deletions
@@ -78,6 +78,12 @@ | |||
78 | 78 | ||
79 | [[projects]] | 79 | [[projects]] |
80 | branch = "master" | 80 | branch = "master" |
81 | name = "github.com/jloup/poloniex" | ||
82 | packages = ["."] | ||
83 | revision = "e75e6fd7991c1d71576ad97de73fc922f24a5fd2" | ||
84 | |||
85 | [[projects]] | ||
86 | branch = "master" | ||
81 | name = "github.com/jloup/utils" | 87 | name = "github.com/jloup/utils" |
82 | packages = ["."] | 88 | packages = ["."] |
83 | revision = "6055a8f761d5892502228aa62249e122f8bd392d" | 89 | revision = "6055a8f761d5892502228aa62249e122f8bd392d" |
@@ -99,6 +105,12 @@ | |||
99 | version = "v1.0.0" | 105 | version = "v1.0.0" |
100 | 106 | ||
101 | [[projects]] | 107 | [[projects]] |
108 | branch = "master" | ||
109 | name = "github.com/shopspring/decimal" | ||
110 | packages = ["."] | ||
111 | revision = "e3482495ff4cba75613e4177ed79825c890058a9" | ||
112 | |||
113 | [[projects]] | ||
102 | name = "github.com/ugorji/go" | 114 | name = "github.com/ugorji/go" |
103 | packages = ["codec"] | 115 | packages = ["codec"] |
104 | revision = "9831f2c3ac1068a78f50999a30db84270f647af6" | 116 | revision = "9831f2c3ac1068a78f50999a30db84270f647af6" |
@@ -112,7 +124,13 @@ | |||
112 | "blowfish", | 124 | "blowfish", |
113 | "ssh/terminal" | 125 | "ssh/terminal" |
114 | ] | 126 | ] |
115 | revision = "650f4a345ab4e5b245a3034b110ebc7299e68186" | 127 | revision = "432090b8f568c018896cd8a0fb0345872bbac6ce" |
128 | |||
129 | [[projects]] | ||
130 | branch = "master" | ||
131 | name = "golang.org/x/net" | ||
132 | packages = ["websocket"] | ||
133 | revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" | ||
116 | 134 | ||
117 | [[projects]] | 135 | [[projects]] |
118 | branch = "master" | 136 | branch = "master" |
@@ -138,6 +156,6 @@ | |||
138 | [solve-meta] | 156 | [solve-meta] |
139 | analyzer-name = "dep" | 157 | analyzer-name = "dep" |
140 | analyzer-version = 1 | 158 | analyzer-version = 1 |
141 | inputs-digest = "c9af022a586632799c6259f6c48eef8dad7080e36b96e8cb5cb905b316c4cb9b" | 159 | inputs-digest = "d3c9b3094ed174bcf1631e3a998a75d557c65c195d7a8fd5ca9912f71f334ce1" |
142 | solver-name = "gps-cdcl" | 160 | solver-name = "gps-cdcl" |
143 | solver-version = 1 | 161 | solver-version = 1 |
@@ -56,3 +56,11 @@ | |||
56 | [prune] | 56 | [prune] |
57 | go-tests = true | 57 | go-tests = true |
58 | unused-packages = true | 58 | unused-packages = true |
59 | |||
60 | [[constraint]] | ||
61 | branch = "master" | ||
62 | name = "github.com/jloup/poloniex" | ||
63 | |||
64 | [[constraint]] | ||
65 | branch = "master" | ||
66 | 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" | |||
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 | ||
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 | |||
6 | BUILD_DIR=build/js | 6 | BUILD_DIR=build/js |
7 | JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx | 7 | JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx |
8 | JS_SRC= cookies.js app.js api.js | 8 | JS_SRC= cookies.js app.js api.js |
9 | STATIC_FILES= index.html style.css cryptocoins.css cryptocoins.ttf cryptocoins.woff cryptocoins.woff2 | ||
9 | JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js)) | 10 | JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js)) |
10 | JS_OBJS=$(addprefix $(BUILD_DIR)/,$(JS_SRC)) | 11 | JS_OBJS=$(addprefix $(BUILD_DIR)/,$(JS_SRC)) |
11 | STATIC_BUILD_DIR=build/static | 12 | STATIC_BUILD_DIR=build/static |
@@ -16,18 +17,18 @@ install: | |||
16 | yarn --version | 17 | yarn --version |
17 | yarn install | 18 | yarn install |
18 | 19 | ||
19 | static: js $(STATIC_BUILD_DIR)/index.html $(STATIC_BUILD_DIR)/style.css | 20 | static: $(STATIC_BUILD_DIR) js $(addprefix $(STATIC_BUILD_DIR)/, $(STATIC_FILES)) |
20 | 21 | ||
21 | js: build/static/main.js | 22 | js: build/static/main.js |
22 | 23 | ||
23 | $(STATIC_BUILD_DIR)/index.html: static/index.html | 24 | $(STATIC_BUILD_DIR)/%: static/% |
24 | cp static/index.html $@ | 25 | cp $< $@ |
25 | 26 | ||
26 | $(STATIC_BUILD_DIR)/style.css: static/style.css | 27 | $(STATIC_BUILD_DIR): |
27 | cp static/style.css $@ | 28 | mkdir -p $(BUILD_DIR) |
29 | mkdir -p $@ | ||
28 | 30 | ||
29 | $(BUILD_DIR)/%.js: $(SRC_DIR)/%.jsx | 31 | $(BUILD_DIR)/%.js: $(SRC_DIR)/%.jsx |
30 | mkdir -p $(@D) | ||
31 | jscs --fix $< | 32 | jscs --fix $< |
32 | babel $< -o $@ | 33 | babel $< -o $@ |
33 | jshint $@ | 34 | jshint $@ |
@@ -42,11 +43,11 @@ build/static/main.js: $(JSX_OBJS) $(JS_OBJS) env/$(ENV).env | |||
42 | -t [ debowerify ] \ | 43 | -t [ debowerify ] \ |
43 | $(BUILD_DIR)/main.js -o $@ | 44 | $(BUILD_DIR)/main.js -o $@ |
44 | 45 | ||
45 | build/webapp.tar.gz: $(STATIC_BUILD_DIR)/main.js $(STATIC_BUILD_DIR)/index.html $(STATIC_BUILD_DIR)/style.css | 46 | build/webapp.tar.gz: $(addprefix $(STATIC_BUILD_DIR)/, $(STATIC_FILES)) build/static/main.js |
46 | tar czf $@ --directory=$(dir $<) $(notdir $^) | 47 | tar czf $@ --directory=$(dir $<) $(notdir $^) |
47 | 48 | ||
48 | release: build/webapp.tar.gz | 49 | release: $(STATIC_BUILD_DIR) build/webapp.tar.gz |
49 | 50 | ||
50 | clean: | 51 | clean: |
51 | rm -rf build | 52 | rm -rf build |
52 | rm -rf node_modules \ No newline at end of file | 53 | 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 = { | |||
53 | return '/market/' + params.name; | 53 | return '/market/' + params.name; |
54 | } | 54 | } |
55 | }, | 55 | }, |
56 | 'MARKET_BALANCE': { | ||
57 | 'type': 'GET', | ||
58 | 'auth': true, | ||
59 | 'parameters': [ | ||
60 | {'name': 'name', 'mandatory': true, 'inquery': false}, | ||
61 | {'name': 'currency', 'mandatory': true, 'inquery': true}, | ||
62 | ], | ||
63 | 'buildUrl': function(params) { | ||
64 | return '/market/' + params.name + '/balance'; | ||
65 | } | ||
66 | }, | ||
56 | 'UPDATE_MARKET': { | 67 | 'UPDATE_MARKET': { |
57 | 'type': 'POST', | 68 | 'type': 'POST', |
58 | 'auth': true, | 69 | '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 @@ | |||
1 | var SignupForm = require('./signup.js').SignupForm; | 1 | var SignupForm = require('./signup.js').SignupForm; |
2 | var SigninForm = require('./signin.js').SigninForm; | 2 | var SigninForm = require('./signin.js').SigninForm; |
3 | var OtpEnrollForm = require('./otp.js').OtpEnrollForm; | 3 | var OtpEnrollForm = require('./otp.js').OtpEnrollForm; |
4 | var PoloniexForm = require('./poloniex.js').PoloniexForm; | 4 | var PoloniexController = require('./poloniex.js').PoloniexController; |
5 | var App = require('./app.js'); | 5 | var App = require('./app.js'); |
6 | var Api = require('./api.js').Api; | 6 | var Api = require('./api.js').Api; |
7 | var cookies = require('./cookies.js'); | 7 | var cookies = require('./cookies.js'); |
8 | 8 | ||
9 | var Logo = React.createClass({ | 9 | var Logo = React.createClass({ |
10 | render: function() { | 10 | render: function() { |
11 | return (<div id='logo'> | 11 | return (<div className='row'> |
12 | <a href='/'>Cryptoportfolio</a> | 12 | <div id='logo' className='offset-4 col-4'> |
13 | <a href='/'>Cryptoportfolio</a> | ||
14 | </div> | ||
13 | </div>); | 15 | </div>); |
14 | } | 16 | } |
15 | }); | 17 | }); |
@@ -49,21 +51,12 @@ App.page('/signout', true, function(context) { | |||
49 | }); | 51 | }); |
50 | 52 | ||
51 | App.page('/me', true, function(context) { | 53 | App.page('/me', true, function(context) { |
52 | Api.Call('MARKET', {'name': 'poloniex'}, function(err, status, data) { | 54 | App.mount( |
53 | if (err) { | 55 | <div> |
54 | console.error(err, data); | 56 | <Logo /> |
55 | return; | 57 | <PoloniexController/> |
56 | } | 58 | </div> |
57 | 59 | ); | |
58 | App.mount( | ||
59 | <div> | ||
60 | <Logo /> | ||
61 | <p>Poloniex</p> | ||
62 | <PoloniexForm apiKey={data.key} apiSecret={data.secret}/> | ||
63 | </div> | ||
64 | ); | ||
65 | |||
66 | }.bind(this)); | ||
67 | }); | 60 | }); |
68 | 61 | ||
69 | App.page('/otp/setup', true, function(context) { | 62 | App.page('/otp/setup', true, function(context) { |
diff --git a/cmd/web/js/otp.jsx b/cmd/web/js/otp.jsx index 2717d9f..a0ee5cc 100644 --- a/cmd/web/js/otp.jsx +++ b/cmd/web/js/otp.jsx | |||
@@ -53,12 +53,12 @@ module.exports.OtpEnrollForm = React.createClass({ | |||
53 | ); | 53 | ); |
54 | } | 54 | } |
55 | return ( | 55 | return ( |
56 | <div className='row otp-enroll justify-content-center'> | 56 | <div className='row otp-enroll'> |
57 | <div className='col-lg-offset-4 col-lg-4 col-md-offset-4 col-md-4 col-sm-offset-4 col-sm-4 col-xs-offset-1 col-xs-10'> | 57 | <div className='offset-4 col-4 col-xs-offset-1 col-xs-10 text-center'> |
58 | {qrCode} | 58 | {qrCode} |
59 | <div className='row justify-content-center'> | 59 | <div className='row justify-content-center'> |
60 | <form role='form' onSubmit={this.handleSubmit}> | 60 | <form role='form' onSubmit={this.handleSubmit}> |
61 | <input className='form-control' type='pass' placeholder='pass' onChange={this.handlePassChange} /> | 61 | <input className='form-control' type='pass' placeholder='code' onChange={this.handlePassChange} /> |
62 | <input className='form-control submit' type='submit' value='Validate' /> | 62 | <input className='form-control submit' type='submit' value='Validate' /> |
63 | <div className={cName} ref='message'>{this.state.msg}</div> | 63 | <div className={cName} ref='message'>{this.state.msg}</div> |
64 | </form> | 64 | </form> |
diff --git a/cmd/web/js/poloniex.jsx b/cmd/web/js/poloniex.jsx index 877198d..8b577b4 100644 --- a/cmd/web/js/poloniex.jsx +++ b/cmd/web/js/poloniex.jsx | |||
@@ -1,49 +1,185 @@ | |||
1 | var Api = require('./api.js').Api; | 1 | var Api = require('./api.js').Api; |
2 | var App = require('./app.js'); | ||
3 | var classNames = require('classnames'); | 2 | var classNames = require('classnames'); |
4 | 3 | ||
5 | module.exports.PoloniexForm = React.createClass({ | 4 | module.exports.PoloniexController = React.createClass({ |
6 | getInitialState: function() { | 5 | getInitialState: function() { |
7 | return {'hideMsg': true, 'msg': '', 'msgOk': false, 'apiSecret': this.props.apiSecret, 'apiKey': this.props.apiKey}; | 6 | return {'apiKey': '', 'apiSecret': '', 'flag': 'loading', 'valueCurrency': null, 'balanceValue': null, 'balance': null}; |
8 | }, | 7 | }, |
9 | handleSubmit: function(e) { | 8 | handleCredentialsChange: function(key, secret) { |
9 | this.setState({'apiKey': key, 'apiSecret': secret}); | ||
10 | }, | ||
11 | handleCredentialsSubmit: function() { | ||
12 | if (!this.state.apiKey || !this.state.apiSecret) { | ||
13 | return; | ||
14 | } | ||
10 | Api.Call('UPDATE_MARKET', {'key': this.state.apiKey, 'secret': this.state.apiSecret, 'name': 'poloniex'}, function(err, status, data) { | 15 | Api.Call('UPDATE_MARKET', {'key': this.state.apiKey, 'secret': this.state.apiSecret, 'name': 'poloniex'}, function(err, status, data) { |
11 | if (err) { | 16 | if (err) { |
12 | console.error(err, data); | 17 | console.error(err, data); |
13 | this.displayMessage(App.errorCodeToMessage(err.code), false); | ||
14 | return; | 18 | return; |
15 | } | 19 | } |
16 | 20 | ||
17 | this.displayMessage('OK', true); | 21 | this.setState({'flag': 'loading', 'valueCurrency': null, 'balanceValue': null, 'balance': null}); |
22 | this.loadBalance(); | ||
23 | }.bind(this)); | ||
24 | }, | ||
25 | loadBalance: function() { | ||
26 | Api.Call('MARKET_BALANCE', {'name': 'poloniex', 'currency': 'BTC'}, function(err, status, data) { | ||
27 | if (err) { | ||
28 | console.error(err, data); | ||
29 | if (err.code === 'invalid_market_credentials') { | ||
30 | this.setState({'flag': 'invalidCredentials', 'valueCurrency': null, 'balanceValue': null, 'balance': null}); | ||
31 | } | ||
32 | return; | ||
33 | } | ||
18 | 34 | ||
35 | this.setState({'flag': 'ok', 'valueCurrency': data.valueCurrency, 'balanceValue': data.value, 'balance': data.balance}); | ||
19 | }.bind(this)); | 36 | }.bind(this)); |
37 | }, | ||
38 | componentDidMount: function() { | ||
39 | Api.Call('MARKET', {'name': 'poloniex'}, function(err, status, data) { | ||
40 | if (err) { | ||
41 | console.error(err, data); | ||
42 | return; | ||
43 | } | ||
44 | |||
45 | var flag = this.state.flag; | ||
46 | if (!data.key || !data.secret) { | ||
47 | flag = 'emptyCredentials'; | ||
48 | } else { | ||
49 | this.loadBalance(); | ||
50 | } | ||
51 | |||
52 | this.setState({'apiKey': data.key, 'apiSecret': data.secret, 'flag': flag}); | ||
53 | }.bind(this)); | ||
54 | }, | ||
55 | render: function() { | ||
56 | var displayText = null; | ||
57 | switch (this.state.flag) { | ||
58 | case 'loading': | ||
59 | displayText = 'Loading data from poloniex...'; | ||
60 | break; | ||
61 | case 'invalidCredentials': | ||
62 | displayText = 'Invalid poloniex credentials'; | ||
63 | break; | ||
64 | case 'emptyCredentials': | ||
65 | displayText = 'Please provide poloniex credentials'; | ||
66 | break; | ||
67 | default: | ||
68 | displayText = null; | ||
69 | } | ||
70 | return ( | ||
71 | <div> | ||
72 | <PoloniexBalance balanceCurrency={this.state.valueCurrency} | ||
73 | balanceValue={this.state.balanceValue} | ||
74 | balance={this.state.balance} | ||
75 | displayText={displayText}/> | ||
76 | <PoloniexCredentialsForm onLoadCredentials={this.onLoadCredentials} | ||
77 | onCredentialsSubmit={this.handleCredentialsSubmit} | ||
78 | onCredentialsChange={this.handleCredentialsChange} | ||
79 | apiSecret={this.state.apiSecret} | ||
80 | apiKey={this.state.apiKey}/> | ||
81 | </div> | ||
82 | ); | ||
83 | } | ||
84 | }); | ||
85 | |||
86 | var PoloniexBalance = React.createClass({ | ||
87 | getInitialState: function() { | ||
88 | return {'hideMsg': true, 'msg': '', 'msgOk': false}; | ||
89 | }, | ||
90 | render: function() { | ||
91 | var dashboard = null; | ||
92 | |||
93 | if (this.props.balanceValue !== null) { | ||
94 | |||
95 | var balance = Object.keys(this.props.balance).map(function(currency) { | ||
96 | return <div key={currency}><i className={classNames('cc', currency)}></i> {this.props.balance[currency]}</div>; | ||
97 | }.bind(this)); | ||
98 | |||
99 | dashboard = ( | ||
100 | <div className='row'> | ||
101 | <div className='col-4 align-self-center h-100'> | ||
102 | <div> | ||
103 | {balance} | ||
104 | </div> | ||
105 | </div> | ||
106 | <div className='offset-1 col-7 h-100 align-self-center'> | ||
107 | <div className='text-center'> | ||
108 | Balance ({this.props.balanceCurrency}): <span>{this.props.balanceValue} <i className={classNames('cc', this.props.balanceCurrency)}></i></span> | ||
109 | </div> | ||
110 | </div> | ||
111 | </div> | ||
112 | ); | ||
113 | } else { | ||
114 | dashboard = ( | ||
115 | <div className='row'> | ||
116 | <div className='col-12 text-center'> | ||
117 | <span>{this.props.displayText}</span> | ||
118 | </div> | ||
119 | </div> | ||
120 | |||
121 | ); | ||
122 | } | ||
123 | |||
124 | return ( | ||
125 | <div className='row'> | ||
126 | <div className='box offset-2 col-8'> | ||
127 | <div className='row'> | ||
128 | <div className='col-4'>Portfolio</div> | ||
129 | </div> | ||
130 | <hr/> | ||
131 | {dashboard} | ||
132 | </div> | ||
133 | </div> | ||
134 | ); | ||
135 | } | ||
136 | }); | ||
137 | |||
138 | module.exports.PoloniexBalance = PoloniexBalance; | ||
139 | |||
140 | var PoloniexCredentialsForm = React.createClass({ | ||
141 | getInitialState: function() { | ||
142 | return {'hideMsg': true, 'msg': '', 'editMode': false, 'msgOk': false}; | ||
143 | }, | ||
144 | handleSubmit: function(e) { | ||
145 | this.props.onCredentialsSubmit(); | ||
146 | this.setState({'editMode': false}); | ||
20 | e.preventDefault(); | 147 | e.preventDefault(); |
21 | }, | 148 | }, |
22 | handleApiKeyChange: function(event) { | 149 | handleApiKeyChange: function(event) { |
23 | this.setState({'apiKey': event.target.value}); | 150 | this.props.onCredentialsChange(event.target.value, this.props.apiSecret); |
24 | }, | 151 | }, |
25 | handleApiSecretChange: function(event) { | 152 | handleApiSecretChange: function(event) { |
26 | this.setState({'apiSecret': event.target.value}); | 153 | this.props.onCredentialsChange(this.props.apiKey, event.target.value); |
27 | }, | 154 | }, |
28 | hideMessage: function() { | 155 | onEditClick: function() { |
29 | this.setState({'hideMsg': true}); | 156 | this.setState({'editMode': true}); |
30 | }, | ||
31 | displayMessage: function(msg, ok) { | ||
32 | this.setState({'msg': msg, 'msgOk': ok, 'hideMsg': false}); | ||
33 | }, | 157 | }, |
34 | render: function() { | 158 | render: function() { |
35 | var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk}); | 159 | var submitType = (this.state.editMode === true) ? 'submit' : 'hidden'; |
160 | var buttonDisplay = (this.state.editMode === true) ? 'none' : 'inline'; | ||
161 | var secretDisplayed = (this.state.editMode === true) ? this.props.apiSecret : 'XXXXXXX'; | ||
162 | var keyDisplayed = (this.state.editMode === true) ? this.props.apiKey : 'XXXXXXX'; | ||
163 | |||
36 | return ( | 164 | return ( |
37 | <div className='row justify-content-center api-credentials-form'> | 165 | <div className='row api-credentials-form'> |
38 | <div className='col-lg-offset-4 col-lg-4 col-md-offset-4 col-md-4 col-sm-offset-4 col-sm-4 col-xs-offset-1 col-xs-10'> | 166 | <div className='offset-3 col-6 box'> |
167 | <span className='text-center'>Poloniex credentials</span> | ||
168 | <hr/> | ||
39 | <form role='form' onSubmit={this.handleSubmit}> | 169 | <form role='form' onSubmit={this.handleSubmit}> |
40 | <input className='form-control' type='text' placeholder='apiKey' value={this.state.apiKey} onChange={this.handleApiKeyChange} /> | 170 | <label className='w-100'>Key: |
41 | <input className='form-control' type='text' placeholder='apiSecret' value={this.state.apiSecret} onChange={this.handleApiSecretChange} /> | 171 | <input className='form-control' type='text' placeholder='key' value={keyDisplayed} onChange={this.handleApiKeyChange} disabled={!this.state.editMode}/> |
42 | <input className='form-control submit' type='submit' value='Save' /> | 172 | </label> |
43 | <div className={cName} ref='message'>{this.state.msg}</div> | 173 | <label className='w-100'>Secret: |
174 | <input className='form-control' type='text' placeholder='secret' value={secretDisplayed} onChange={this.handleApiSecretChange} disabled={!this.state.editMode}/> | ||
175 | </label> | ||
176 | <input className='form-control submit' type={submitType} value='Save' /> | ||
177 | <button className='form-control submit' style={{display: buttonDisplay}} onSubmit={null} onClick={this.onEditClick} type='button'>Show/Edit</button> | ||
44 | </form> | 178 | </form> |
45 | </div> | 179 | </div> |
46 | </div> | 180 | </div> |
47 | ); | 181 | ); |
48 | } | 182 | } |
49 | }); | 183 | }); |
184 | |||
185 | module.exports.PoloniexCredentialsForm = PoloniexCredentialsForm; | ||
diff --git a/cmd/web/js/signin.jsx b/cmd/web/js/signin.jsx index 443a461..a2cfd1b 100644 --- a/cmd/web/js/signin.jsx +++ b/cmd/web/js/signin.jsx | |||
@@ -35,8 +35,8 @@ module.exports.SigninForm = React.createClass({ | |||
35 | render: function() { | 35 | render: function() { |
36 | var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk}); | 36 | var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk}); |
37 | return ( | 37 | return ( |
38 | <div className='row justify-content-center sign-in'> | 38 | <div className='row sign-in'> |
39 | <div className='col-lg-offset-4 col-lg-4 col-md-offset-4 col-md-4 col-sm-offset-4 col-sm-4 col-xs-offset-1 col-xs-10'> | 39 | <div className='offset-4 col-4 col-xs-offset-1 col-xs-10 text-center'> |
40 | <form role='form' onSubmit={this.handleSubmit}> | 40 | <form role='form' onSubmit={this.handleSubmit}> |
41 | <input className='form-control' type='email' placeholder='email' onChange={this.handleEmailChange} /> | 41 | <input className='form-control' type='email' placeholder='email' onChange={this.handleEmailChange} /> |
42 | <input className='form-control' type='password' placeholder='password' onChange={this.handlePasswordChange} /> | 42 | <input className='form-control' type='password' placeholder='password' onChange={this.handlePasswordChange} /> |
diff --git a/cmd/web/js/signup.jsx b/cmd/web/js/signup.jsx index 149125a..404a828 100644 --- a/cmd/web/js/signup.jsx +++ b/cmd/web/js/signup.jsx | |||
@@ -40,8 +40,8 @@ module.exports.SignupForm = React.createClass({ | |||
40 | render: function() { | 40 | render: function() { |
41 | var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk}); | 41 | var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk}); |
42 | return ( | 42 | return ( |
43 | <div className='row justify-content-center sign-in'> | 43 | <div className='row sign-in'> |
44 | <div className='col-lg-offset-4 col-lg-4 col-md-offset-4 col-md-4 col-sm-offset-4 col-sm-4 col-xs-offset-1 col-xs-10'> | 44 | <div className='offset-4 col-4 col-xs-offset-1 col-xs-10 text-center'> |
45 | <form role='form' onSubmit={this.handleSubmit}> | 45 | <form role='form' onSubmit={this.handleSubmit}> |
46 | <input className='form-control' type='email' placeholder='email' onChange={this.handleEmailChange} /> | 46 | <input className='form-control' type='email' placeholder='email' onChange={this.handleEmailChange} /> |
47 | <input className='form-control' type='password' placeholder='password' onChange={this.handlePasswordChange} /> | 47 | <input className='form-control' type='password' placeholder='password' onChange={this.handlePasswordChange} /> |
diff --git a/cmd/web/static/cryptocoins.css b/cmd/web/static/cryptocoins.css new file mode 100644 index 0000000..6829169 --- /dev/null +++ b/cmd/web/static/cryptocoins.css | |||
@@ -0,0 +1,1150 @@ | |||
1 | /*! Cryptocoins - cryptocurrency icon font | https://github.com/allienworks/cryptocoins */ | ||
2 | |||
3 | @font-face { | ||
4 | font-family: "cryptocoins"; | ||
5 | src: url('cryptocoins.woff2') format('woff2'), | ||
6 | url('cryptocoins.woff') format('woff'), | ||
7 | url('cryptocoins.ttf') format('truetype'); | ||
8 | } | ||
9 | |||
10 | /* .cc:before { */ | ||
11 | .cc::before { | ||
12 | font-family: "cryptocoins"; | ||
13 | -webkit-font-smoothing: antialiased; | ||
14 | -moz-osx-font-smoothing: grayscale; | ||
15 | font-style: normal; | ||
16 | font-variant: normal; | ||
17 | font-weight: normal; | ||
18 | /* speak: none; only necessary if not using the private unicode range (firstGlyph option) */ | ||
19 | text-decoration: none; | ||
20 | text-transform: none; | ||
21 | } | ||
22 | |||
23 | |||
24 | .cc.ADA-alt::before { | ||
25 | content: "\E001"; | ||
26 | } | ||
27 | |||
28 | .cc.ADA::before { | ||
29 | content: "\E002"; | ||
30 | } | ||
31 | |||
32 | .cc.ADC-alt::before { | ||
33 | content: "\E003"; | ||
34 | } | ||
35 | |||
36 | .cc.ADC::before { | ||
37 | content: "\E004"; | ||
38 | } | ||
39 | |||
40 | .cc.AEON-alt::before { | ||
41 | content: "\E005"; | ||
42 | } | ||
43 | |||
44 | .cc.AEON::before { | ||
45 | content: "\E006"; | ||
46 | } | ||
47 | |||
48 | .cc.AMP-alt::before { | ||
49 | content: "\E007"; | ||
50 | } | ||
51 | |||
52 | .cc.AMP::before { | ||
53 | content: "\E008"; | ||
54 | } | ||
55 | |||
56 | .cc.ANC-alt::before { | ||
57 | content: "\E009"; | ||
58 | } | ||
59 | |||
60 | .cc.ANC::before { | ||
61 | content: "\E00A"; | ||
62 | } | ||
63 | |||
64 | .cc.ARCH-alt::before { | ||
65 | content: "\E00B"; | ||
66 | } | ||
67 | |||
68 | .cc.ARCH::before { | ||
69 | content: "\E00C"; | ||
70 | } | ||
71 | |||
72 | .cc.ARDR-alt::before { | ||
73 | content: "\E00D"; | ||
74 | } | ||
75 | |||
76 | .cc.ARDR::before { | ||
77 | content: "\E00E"; | ||
78 | } | ||
79 | |||
80 | .cc.ARK-alt::before { | ||
81 | content: "\E00F"; | ||
82 | } | ||
83 | |||
84 | .cc.ARK::before { | ||
85 | content: "\E010"; | ||
86 | } | ||
87 | |||
88 | .cc.AUR-alt::before { | ||
89 | content: "\E011"; | ||
90 | } | ||
91 | |||
92 | .cc.AUR::before { | ||
93 | content: "\E012"; | ||
94 | } | ||
95 | |||
96 | .cc.BANX-alt::before { | ||
97 | content: "\E013"; | ||
98 | } | ||
99 | |||
100 | .cc.BANX::before { | ||
101 | content: "\E014"; | ||
102 | } | ||
103 | |||
104 | .cc.BAT-alt::before { | ||
105 | content: "\E015"; | ||
106 | } | ||
107 | |||
108 | .cc.BAT::before { | ||
109 | content: "\E016"; | ||
110 | } | ||
111 | |||
112 | .cc.BAY-alt::before { | ||
113 | content: "\E017"; | ||
114 | } | ||
115 | |||
116 | .cc.BAY::before { | ||
117 | content: "\E018"; | ||
118 | } | ||
119 | |||
120 | .cc.BC-alt::before { | ||
121 | content: "\E019"; | ||
122 | } | ||
123 | |||
124 | .cc.BC::before { | ||
125 | content: "\E01A"; | ||
126 | } | ||
127 | |||
128 | .cc.BCH-alt::before { | ||
129 | content: "\E01B"; | ||
130 | } | ||
131 | |||
132 | .cc.BCH::before { | ||
133 | content: "\E01C"; | ||
134 | } | ||
135 | |||
136 | .cc.BCN-alt::before { | ||
137 | content: "\E01D"; | ||
138 | } | ||
139 | |||
140 | .cc.BCN::before { | ||
141 | content: "\E01E"; | ||
142 | } | ||
143 | |||
144 | .cc.BFT-alt::before { | ||
145 | content: "\E01F"; | ||
146 | } | ||
147 | |||
148 | .cc.BFT::before { | ||
149 | content: "\E020"; | ||
150 | } | ||
151 | |||
152 | .cc.BRK-alt::before { | ||
153 | content: "\E021"; | ||
154 | } | ||
155 | |||
156 | .cc.BRK::before { | ||
157 | content: "\E022"; | ||
158 | } | ||
159 | |||
160 | .cc.BRX-alt::before { | ||
161 | content: "\E023"; | ||
162 | } | ||
163 | |||
164 | .cc.BRX::before { | ||
165 | content: "\E024"; | ||
166 | } | ||
167 | |||
168 | .cc.BSD-alt::before { | ||
169 | content: "\E025"; | ||
170 | } | ||
171 | |||
172 | .cc.BSD::before { | ||
173 | content: "\E026"; | ||
174 | } | ||
175 | |||
176 | .cc.BTA::before { | ||
177 | content: "\E027"; | ||
178 | } | ||
179 | |||
180 | .cc.BTC-alt::before { | ||
181 | content: "\E028"; | ||
182 | } | ||
183 | |||
184 | .cc.BTC::before { | ||
185 | content: "\E029"; | ||
186 | } | ||
187 | |||
188 | .cc.BTCD-alt::before { | ||
189 | content: "\E02A"; | ||
190 | } | ||
191 | |||
192 | .cc.BTCD::before { | ||
193 | content: "\E02B"; | ||
194 | } | ||
195 | |||
196 | .cc.BTM-alt::before { | ||
197 | content: "\E02C"; | ||
198 | } | ||
199 | |||
200 | .cc.BTM::before { | ||
201 | content: "\E02D"; | ||
202 | } | ||
203 | |||
204 | .cc.BTS-alt::before { | ||
205 | content: "\E02E"; | ||
206 | } | ||
207 | |||
208 | .cc.BTS::before { | ||
209 | content: "\E02F"; | ||
210 | } | ||
211 | |||
212 | .cc.CLAM-alt::before { | ||
213 | content: "\E030"; | ||
214 | } | ||
215 | |||
216 | .cc.CLAM::before { | ||
217 | content: "\E031"; | ||
218 | } | ||
219 | |||
220 | .cc.CLOAK-alt::before { | ||
221 | content: "\E032"; | ||
222 | } | ||
223 | |||
224 | .cc.CLOAK::before { | ||
225 | content: "\E033"; | ||
226 | } | ||
227 | |||
228 | .cc.DAO-alt::before { | ||
229 | content: "\E034"; | ||
230 | } | ||
231 | |||
232 | .cc.DAO::before { | ||
233 | content: "\E035"; | ||
234 | } | ||
235 | |||
236 | .cc.DASH-alt::before { | ||
237 | content: "\E036"; | ||
238 | } | ||
239 | |||
240 | .cc.DASH::before { | ||
241 | content: "\E037"; | ||
242 | } | ||
243 | |||
244 | .cc.DCR-alt::before { | ||
245 | content: "\E038"; | ||
246 | } | ||
247 | |||
248 | .cc.DCR::before { | ||
249 | content: "\E039"; | ||
250 | } | ||
251 | |||
252 | .cc.DCT-alt::before { | ||
253 | content: "\E03A"; | ||
254 | } | ||
255 | |||
256 | .cc.DCT::before { | ||
257 | content: "\E03B"; | ||
258 | } | ||
259 | |||
260 | .cc.DGB-alt::before { | ||
261 | content: "\E03C"; | ||
262 | } | ||
263 | |||
264 | .cc.DGB::before { | ||
265 | content: "\E03D"; | ||
266 | } | ||
267 | |||
268 | .cc.DGD::before { | ||
269 | content: "\E03E"; | ||
270 | } | ||
271 | |||
272 | .cc.DGX::before { | ||
273 | content: "\E03F"; | ||
274 | } | ||
275 | |||
276 | .cc.DMD-alt::before { | ||
277 | content: "\E040"; | ||
278 | } | ||
279 | |||
280 | .cc.DMD::before { | ||
281 | content: "\E041"; | ||
282 | } | ||
283 | |||
284 | .cc.DOGE-alt::before { | ||
285 | content: "\E042"; | ||
286 | } | ||
287 | |||
288 | .cc.DOGE::before { | ||
289 | content: "\E043"; | ||
290 | } | ||
291 | |||
292 | .cc.EMC-alt::before { | ||
293 | content: "\E044"; | ||
294 | } | ||
295 | |||
296 | .cc.EMC::before { | ||
297 | content: "\E045"; | ||
298 | } | ||
299 | |||
300 | .cc.EOS-alt::before { | ||
301 | content: "\E046"; | ||
302 | } | ||
303 | |||
304 | .cc.EOS::before { | ||
305 | content: "\E047"; | ||
306 | } | ||
307 | |||
308 | .cc.ERC-alt::before { | ||
309 | content: "\E048"; | ||
310 | } | ||
311 | |||
312 | .cc.ERC::before { | ||
313 | content: "\E049"; | ||
314 | } | ||
315 | |||
316 | .cc.ETC-alt::before { | ||
317 | content: "\E04A"; | ||
318 | } | ||
319 | |||
320 | .cc.ETC::before { | ||
321 | content: "\E04B"; | ||
322 | } | ||
323 | |||
324 | .cc.ETH-alt::before { | ||
325 | content: "\E04C"; | ||
326 | } | ||
327 | |||
328 | .cc.ETH::before { | ||
329 | content: "\E04D"; | ||
330 | } | ||
331 | |||
332 | .cc.FC2-alt::before { | ||
333 | content: "\E04E"; | ||
334 | } | ||
335 | |||
336 | .cc.FC2::before { | ||
337 | content: "\E04F"; | ||
338 | } | ||
339 | |||
340 | .cc.FCT-alt::before { | ||
341 | content: "\E050"; | ||
342 | } | ||
343 | |||
344 | .cc.FCT::before { | ||
345 | content: "\E051"; | ||
346 | } | ||
347 | |||
348 | .cc.FLO-alt::before { | ||
349 | content: "\E052"; | ||
350 | } | ||
351 | |||
352 | .cc.FLO::before { | ||
353 | content: "\E053"; | ||
354 | } | ||
355 | |||
356 | .cc.FRK-alt::before { | ||
357 | content: "\E054"; | ||
358 | } | ||
359 | |||
360 | .cc.FRK::before { | ||
361 | content: "\E055"; | ||
362 | } | ||
363 | |||
364 | .cc.FTC-alt::before { | ||
365 | content: "\E056"; | ||
366 | } | ||
367 | |||
368 | .cc.FTC::before { | ||
369 | content: "\E057"; | ||
370 | } | ||
371 | |||
372 | .cc.GAME-alt::before { | ||
373 | content: "\E058"; | ||
374 | } | ||
375 | |||
376 | .cc.GAME::before { | ||
377 | content: "\E059"; | ||
378 | } | ||
379 | |||
380 | .cc.GBYTE-alt::before { | ||
381 | content: "\E05A"; | ||
382 | } | ||
383 | |||
384 | .cc.GBYTE::before { | ||
385 | content: "\E05B"; | ||
386 | } | ||
387 | |||
388 | .cc.GDC-alt::before { | ||
389 | content: "\E05C"; | ||
390 | } | ||
391 | |||
392 | .cc.GDC::before { | ||
393 | content: "\E05D"; | ||
394 | } | ||
395 | |||
396 | .cc.GEMZ-alt::before { | ||
397 | content: "\E05E"; | ||
398 | } | ||
399 | |||
400 | .cc.GEMZ::before { | ||
401 | content: "\E05F"; | ||
402 | } | ||
403 | |||
404 | .cc.GLD-alt::before { | ||
405 | content: "\E060"; | ||
406 | } | ||
407 | |||
408 | .cc.GLD::before { | ||
409 | content: "\E061"; | ||
410 | } | ||
411 | |||
412 | .cc.GNO-alt::before { | ||
413 | content: "\E062"; | ||
414 | } | ||
415 | |||
416 | .cc.GNO::before { | ||
417 | content: "\E063"; | ||
418 | } | ||
419 | |||
420 | .cc.GNT-alt::before { | ||
421 | content: "\E064"; | ||
422 | } | ||
423 | |||
424 | .cc.GNT::before { | ||
425 | content: "\E065"; | ||
426 | } | ||
427 | |||
428 | .cc.GOLOS-alt::before { | ||
429 | content: "\E066"; | ||
430 | } | ||
431 | |||
432 | .cc.GOLOS::before { | ||
433 | content: "\E067"; | ||
434 | } | ||
435 | |||
436 | .cc.GRC-alt::before { | ||
437 | content: "\E068"; | ||
438 | } | ||
439 | |||
440 | .cc.GRC::before { | ||
441 | content: "\E069"; | ||
442 | } | ||
443 | |||
444 | .cc.GRS::before { | ||
445 | content: "\E06A"; | ||
446 | } | ||
447 | |||
448 | .cc.HEAT-alt::before { | ||
449 | content: "\E06B"; | ||
450 | } | ||
451 | |||
452 | .cc.HEAT::before { | ||
453 | content: "\E06C"; | ||
454 | } | ||
455 | |||
456 | .cc.ICN-alt::before { | ||
457 | content: "\E06D"; | ||
458 | } | ||
459 | |||
460 | .cc.ICN::before { | ||
461 | content: "\E06E"; | ||
462 | } | ||
463 | |||
464 | .cc.IFC-alt::before { | ||
465 | content: "\E06F"; | ||
466 | } | ||
467 | |||
468 | .cc.IFC::before { | ||
469 | content: "\E070"; | ||
470 | } | ||
471 | |||
472 | .cc.INCNT-alt::before { | ||
473 | content: "\E071"; | ||
474 | } | ||
475 | |||
476 | .cc.INCNT::before { | ||
477 | content: "\E072"; | ||
478 | } | ||
479 | |||
480 | .cc.IOC-alt::before { | ||
481 | content: "\E073"; | ||
482 | } | ||
483 | |||
484 | .cc.IOC::before { | ||
485 | content: "\E074"; | ||
486 | } | ||
487 | |||
488 | .cc.IOTA-alt::before { | ||
489 | content: "\E075"; | ||
490 | } | ||
491 | |||
492 | .cc.IOTA::before { | ||
493 | content: "\E076"; | ||
494 | } | ||
495 | |||
496 | .cc.JBS-alt::before { | ||
497 | content: "\E077"; | ||
498 | } | ||
499 | |||
500 | .cc.JBS::before { | ||
501 | content: "\E078"; | ||
502 | } | ||
503 | |||
504 | .cc.KMD-alt::before { | ||
505 | content: "\E079"; | ||
506 | } | ||
507 | |||
508 | .cc.KMD::before { | ||
509 | content: "\E07A"; | ||
510 | } | ||
511 | |||
512 | .cc.KOBO::before { | ||
513 | content: "\E07B"; | ||
514 | } | ||
515 | |||
516 | .cc.KORE-alt::before { | ||
517 | content: "\E07C"; | ||
518 | } | ||
519 | |||
520 | .cc.KORE::before { | ||
521 | content: "\E07D"; | ||
522 | } | ||
523 | |||
524 | .cc.LBC-alt::before { | ||
525 | content: "\E07E"; | ||
526 | } | ||
527 | |||
528 | .cc.LBC::before { | ||
529 | content: "\E07F"; | ||
530 | } | ||
531 | |||
532 | .cc.LDOGE-alt::before { | ||
533 | content: "\E080"; | ||
534 | } | ||
535 | |||
536 | .cc.LDOGE::before { | ||
537 | content: "\E081"; | ||
538 | } | ||
539 | |||
540 | .cc.LSK-alt::before { | ||
541 | content: "\E082"; | ||
542 | } | ||
543 | |||
544 | .cc.LSK::before { | ||
545 | content: "\E083"; | ||
546 | } | ||
547 | |||
548 | .cc.LTC-alt::before { | ||
549 | content: "\E084"; | ||
550 | } | ||
551 | |||
552 | .cc.LTC::before { | ||
553 | content: "\E085"; | ||
554 | } | ||
555 | |||
556 | .cc.MAID-alt::before { | ||
557 | content: "\E086"; | ||
558 | } | ||
559 | |||
560 | .cc.MAID::before { | ||
561 | content: "\E087"; | ||
562 | } | ||
563 | |||
564 | .cc.MCO-alt::before { | ||
565 | content: "\E088"; | ||
566 | } | ||
567 | |||
568 | .cc.MCO::before { | ||
569 | content: "\E089"; | ||
570 | } | ||
571 | |||
572 | .cc.MINT-alt::before { | ||
573 | content: "\E08A"; | ||
574 | } | ||
575 | |||
576 | .cc.MINT::before { | ||
577 | content: "\E08B"; | ||
578 | } | ||
579 | |||
580 | .cc.MONA-alt::before { | ||
581 | content: "\E08C"; | ||
582 | } | ||
583 | |||
584 | .cc.MONA::before { | ||
585 | content: "\E08D"; | ||
586 | } | ||
587 | |||
588 | .cc.MRC::before { | ||
589 | content: "\E08E"; | ||
590 | } | ||
591 | |||
592 | .cc.MSC-alt::before { | ||
593 | content: "\E08F"; | ||
594 | } | ||
595 | |||
596 | .cc.MSC::before { | ||
597 | content: "\E090"; | ||
598 | } | ||
599 | |||
600 | .cc.MTR-alt::before { | ||
601 | content: "\E091"; | ||
602 | } | ||
603 | |||
604 | .cc.MTR::before { | ||
605 | content: "\E092"; | ||
606 | } | ||
607 | |||
608 | .cc.MUE-alt::before { | ||
609 | content: "\E093"; | ||
610 | } | ||
611 | |||
612 | .cc.MUE::before { | ||
613 | content: "\E094"; | ||
614 | } | ||
615 | |||
616 | .cc.NBT::before { | ||
617 | content: "\E095"; | ||
618 | } | ||
619 | |||
620 | .cc.NEO-alt::before { | ||
621 | content: "\E096"; | ||
622 | } | ||
623 | |||
624 | .cc.NEO::before { | ||
625 | content: "\E097"; | ||
626 | } | ||
627 | |||
628 | .cc.NEOS-alt::before { | ||
629 | content: "\E098"; | ||
630 | } | ||
631 | |||
632 | .cc.NEOS::before { | ||
633 | content: "\E099"; | ||
634 | } | ||
635 | |||
636 | .cc.NEU-alt::before { | ||
637 | content: "\E09A"; | ||
638 | } | ||
639 | |||
640 | .cc.NEU::before { | ||
641 | content: "\E09B"; | ||
642 | } | ||
643 | |||
644 | .cc.NLG-alt::before { | ||
645 | content: "\E09C"; | ||
646 | } | ||
647 | |||
648 | .cc.NLG::before { | ||
649 | content: "\E09D"; | ||
650 | } | ||
651 | |||
652 | .cc.NMC-alt::before { | ||
653 | content: "\E09E"; | ||
654 | } | ||
655 | |||
656 | .cc.NMC::before { | ||
657 | content: "\E09F"; | ||
658 | } | ||
659 | |||
660 | .cc.NOTE-alt::before { | ||
661 | content: "\E0A0"; | ||
662 | } | ||
663 | |||
664 | .cc.NOTE::before { | ||
665 | content: "\E0A1"; | ||
666 | } | ||
667 | |||
668 | .cc.NVC-alt::before { | ||
669 | content: "\E0A2"; | ||
670 | } | ||
671 | |||
672 | .cc.NVC::before { | ||
673 | content: "\E0A3"; | ||
674 | } | ||
675 | |||
676 | .cc.NXT-alt::before { | ||
677 | content: "\E0A4"; | ||
678 | } | ||
679 | |||
680 | .cc.NXT::before { | ||
681 | content: "\E0A5"; | ||
682 | } | ||
683 | |||
684 | .cc.OK-alt::before { | ||
685 | content: "\E0A6"; | ||
686 | } | ||
687 | |||
688 | .cc.OK::before { | ||
689 | content: "\E0A7"; | ||
690 | } | ||
691 | |||
692 | .cc.OMG-alt::before { | ||
693 | content: "\E0A8"; | ||
694 | } | ||
695 | |||
696 | .cc.OMG::before { | ||
697 | content: "\E0A9"; | ||
698 | } | ||
699 | |||
700 | .cc.OMNI-alt::before { | ||
701 | content: "\E0AA"; | ||
702 | } | ||
703 | |||
704 | .cc.OMNI::before { | ||
705 | content: "\E0AB"; | ||
706 | } | ||
707 | |||
708 | .cc.OPAL-alt::before { | ||
709 | content: "\E0AC"; | ||
710 | } | ||
711 | |||
712 | .cc.OPAL::before { | ||
713 | content: "\E0AD"; | ||
714 | } | ||
715 | |||
716 | .cc.PART-alt::before { | ||
717 | content: "\E0AE"; | ||
718 | } | ||
719 | |||
720 | .cc.PART::before { | ||
721 | content: "\E0AF"; | ||
722 | } | ||
723 | |||
724 | .cc.PIGGY-alt::before { | ||
725 | content: "\E0B0"; | ||
726 | } | ||
727 | |||
728 | .cc.PIGGY::before { | ||
729 | content: "\E0B1"; | ||
730 | } | ||
731 | |||
732 | .cc.PINK-alt::before { | ||
733 | content: "\E0B2"; | ||
734 | } | ||
735 | |||
736 | .cc.PINK::before { | ||
737 | content: "\E0B3"; | ||
738 | } | ||
739 | |||
740 | .cc.PIVX-alt::before { | ||
741 | content: "\E0B4"; | ||
742 | } | ||
743 | |||
744 | .cc.PIVX::before { | ||
745 | content: "\E0B5"; | ||
746 | } | ||
747 | |||
748 | .cc.POT-alt::before { | ||
749 | content: "\E0B6"; | ||
750 | } | ||
751 | |||
752 | .cc.POT::before { | ||
753 | content: "\E0B7"; | ||
754 | } | ||
755 | |||
756 | .cc.PPC-alt::before { | ||
757 | content: "\E0B8"; | ||
758 | } | ||
759 | |||
760 | .cc.PPC::before { | ||
761 | content: "\E0B9"; | ||
762 | } | ||
763 | |||
764 | .cc.QRK-alt::before { | ||
765 | content: "\E0BA"; | ||
766 | } | ||
767 | |||
768 | .cc.QRK::before { | ||
769 | content: "\E0BB"; | ||
770 | } | ||
771 | |||
772 | .cc.QTUM-alt::before { | ||
773 | content: "\E0BC"; | ||
774 | } | ||
775 | |||
776 | .cc.QTUM::before { | ||
777 | content: "\E0BD"; | ||
778 | } | ||
779 | |||
780 | .cc.RADS-alt::before { | ||
781 | content: "\E0BE"; | ||
782 | } | ||
783 | |||
784 | .cc.RADS::before { | ||
785 | content: "\E0BF"; | ||
786 | } | ||
787 | |||
788 | .cc.RBIES-alt::before { | ||
789 | content: "\E0C0"; | ||
790 | } | ||
791 | |||
792 | .cc.RBIES::before { | ||
793 | content: "\E0C1"; | ||
794 | } | ||
795 | |||
796 | .cc.RBT-alt::before { | ||
797 | content: "\E0C2"; | ||
798 | } | ||
799 | |||
800 | .cc.RBT::before { | ||
801 | content: "\E0C3"; | ||
802 | } | ||
803 | |||
804 | .cc.RBY-alt::before { | ||
805 | content: "\E0C4"; | ||
806 | } | ||
807 | |||
808 | .cc.RBY::before { | ||
809 | content: "\E0C5"; | ||
810 | } | ||
811 | |||
812 | .cc.RDD-alt::before { | ||
813 | content: "\E0C6"; | ||
814 | } | ||
815 | |||
816 | .cc.RDD::before { | ||
817 | content: "\E0C7"; | ||
818 | } | ||
819 | |||
820 | .cc.REP-alt::before { | ||
821 | content: "\E0C8"; | ||
822 | } | ||
823 | |||
824 | .cc.REP::before { | ||
825 | content: "\E0C9"; | ||
826 | } | ||
827 | |||
828 | .cc.RISE-alt::before { | ||
829 | content: "\E0CA"; | ||
830 | } | ||
831 | |||
832 | .cc.RISE::before { | ||
833 | content: "\E0CB"; | ||
834 | } | ||
835 | |||
836 | .cc.SALT-alt::before { | ||
837 | content: "\E0CC"; | ||
838 | } | ||
839 | |||
840 | .cc.SALT::before { | ||
841 | content: "\E0CD"; | ||
842 | } | ||
843 | |||
844 | .cc.SAR-alt::before { | ||
845 | content: "\E0CE"; | ||
846 | } | ||
847 | |||
848 | .cc.SAR::before { | ||
849 | content: "\E0CF"; | ||
850 | } | ||
851 | |||
852 | .cc.SCOT-alt::before { | ||
853 | content: "\E0D0"; | ||
854 | } | ||
855 | |||
856 | .cc.SCOT::before { | ||
857 | content: "\E0D1"; | ||
858 | } | ||
859 | |||
860 | .cc.SDC-alt::before { | ||
861 | content: "\E0D2"; | ||
862 | } | ||
863 | |||
864 | .cc.SDC::before { | ||
865 | content: "\E0D3"; | ||
866 | } | ||
867 | |||
868 | .cc.SIA-alt::before { | ||
869 | content: "\E0D4"; | ||
870 | } | ||
871 | |||
872 | .cc.SIA::before { | ||
873 | content: "\E0D5"; | ||
874 | } | ||
875 | |||
876 | .cc.SJCX-alt::before { | ||
877 | content: "\E0D6"; | ||
878 | } | ||
879 | |||
880 | .cc.SJCX::before { | ||
881 | content: "\E0D7"; | ||
882 | } | ||
883 | |||
884 | .cc.SLG-alt::before { | ||
885 | content: "\E0D8"; | ||
886 | } | ||
887 | |||
888 | .cc.SLG::before { | ||
889 | content: "\E0D9"; | ||
890 | } | ||
891 | |||
892 | .cc.SLS-alt::before { | ||
893 | content: "\E0DA"; | ||
894 | } | ||
895 | |||
896 | .cc.SLS::before { | ||
897 | content: "\E0DB"; | ||
898 | } | ||
899 | |||
900 | .cc.SNRG-alt::before { | ||
901 | content: "\E0DC"; | ||
902 | } | ||
903 | |||
904 | .cc.SNRG::before { | ||
905 | content: "\E0DD"; | ||
906 | } | ||
907 | |||
908 | .cc.START-alt::before { | ||
909 | content: "\E0DE"; | ||
910 | } | ||
911 | |||
912 | .cc.START::before { | ||
913 | content: "\E0DF"; | ||
914 | } | ||
915 | |||
916 | .cc.STEEM-alt::before { | ||
917 | content: "\E0E0"; | ||
918 | } | ||
919 | |||
920 | .cc.STEEM::before { | ||
921 | content: "\E0E1"; | ||
922 | } | ||
923 | |||
924 | .cc.STR-alt::before { | ||
925 | content: "\E0E2"; | ||
926 | } | ||
927 | |||
928 | .cc.STR::before { | ||
929 | content: "\E0E3"; | ||
930 | } | ||
931 | |||
932 | .cc.STRAT-alt::before { | ||
933 | content: "\E0E4"; | ||
934 | } | ||
935 | |||
936 | .cc.STRAT::before { | ||
937 | content: "\E0E5"; | ||
938 | } | ||
939 | |||
940 | .cc.SWIFT-alt::before { | ||
941 | content: "\E0E6"; | ||
942 | } | ||
943 | |||
944 | .cc.SWIFT::before { | ||
945 | content: "\E0E7"; | ||
946 | } | ||
947 | |||
948 | .cc.SYNC-alt::before { | ||
949 | content: "\E0E8"; | ||
950 | } | ||
951 | |||
952 | .cc.SYNC::before { | ||
953 | content: "\E0E9"; | ||
954 | } | ||
955 | |||
956 | .cc.SYS-alt::before { | ||
957 | content: "\E0EA"; | ||
958 | } | ||
959 | |||
960 | .cc.SYS::before { | ||
961 | content: "\E0EB"; | ||
962 | } | ||
963 | |||
964 | .cc.TRIG-alt::before { | ||
965 | content: "\E0EC"; | ||
966 | } | ||
967 | |||
968 | .cc.TRIG::before { | ||
969 | content: "\E0ED"; | ||
970 | } | ||
971 | |||
972 | .cc.TX-alt::before { | ||
973 | content: "\E0EE"; | ||
974 | } | ||
975 | |||
976 | .cc.TX::before { | ||
977 | content: "\E0EF"; | ||
978 | } | ||
979 | |||
980 | .cc.UBQ-alt::before { | ||
981 | content: "\E0F0"; | ||
982 | } | ||
983 | |||
984 | .cc.UBQ::before { | ||
985 | content: "\E0F1"; | ||
986 | } | ||
987 | |||
988 | .cc.UNITY-alt::before { | ||
989 | content: "\E0F2"; | ||
990 | } | ||
991 | |||
992 | .cc.UNITY::before { | ||
993 | content: "\E0F3"; | ||
994 | } | ||
995 | |||
996 | .cc.USDT-alt::before { | ||
997 | content: "\E0F4"; | ||
998 | } | ||
999 | |||
1000 | .cc.USDT::before { | ||
1001 | content: "\E0F5"; | ||
1002 | } | ||
1003 | |||
1004 | .cc.VIOR-alt::before { | ||
1005 | content: "\E0F6"; | ||
1006 | } | ||
1007 | |||
1008 | .cc.VIOR::before { | ||
1009 | content: "\E0F7"; | ||
1010 | } | ||
1011 | |||
1012 | .cc.VNL-alt::before { | ||
1013 | content: "\E0F8"; | ||
1014 | } | ||
1015 | |||
1016 | .cc.VNL::before { | ||
1017 | content: "\E0F9"; | ||
1018 | } | ||
1019 | |||
1020 | .cc.VPN-alt::before { | ||
1021 | content: "\E0FA"; | ||
1022 | } | ||
1023 | |||
1024 | .cc.VPN::before { | ||
1025 | content: "\E0FB"; | ||
1026 | } | ||
1027 | |||
1028 | .cc.VRC-alt::before { | ||
1029 | content: "\E0FC"; | ||
1030 | } | ||
1031 | |||
1032 | .cc.VRC::before { | ||
1033 | content: "\E0FD"; | ||
1034 | } | ||
1035 | |||
1036 | .cc.VTC-alt::before { | ||
1037 | content: "\E0FE"; | ||
1038 | } | ||
1039 | |||
1040 | .cc.VTC::before { | ||
1041 | content: "\E0FF"; | ||
1042 | } | ||
1043 | |||
1044 | .cc.WAVES-alt::before { | ||
1045 | content: "\E100"; | ||
1046 | } | ||
1047 | |||
1048 | .cc.WAVES::before { | ||
1049 | content: "\E101"; | ||
1050 | } | ||
1051 | |||
1052 | .cc.XAI-alt::before { | ||
1053 | content: "\E102"; | ||
1054 | } | ||
1055 | |||
1056 | .cc.XAI::before { | ||
1057 | content: "\E103"; | ||
1058 | } | ||
1059 | |||
1060 | .cc.XBS-alt::before { | ||
1061 | content: "\E104"; | ||
1062 | } | ||
1063 | |||
1064 | .cc.XBS::before { | ||
1065 | content: "\E105"; | ||
1066 | } | ||
1067 | |||
1068 | .cc.XCP-alt::before { | ||
1069 | content: "\E106"; | ||
1070 | } | ||
1071 | |||
1072 | .cc.XCP::before { | ||
1073 | content: "\E107"; | ||
1074 | } | ||
1075 | |||
1076 | .cc.XEM-alt::before { | ||
1077 | content: "\E108"; | ||
1078 | } | ||
1079 | |||
1080 | .cc.XEM::before { | ||
1081 | content: "\E109"; | ||
1082 | } | ||
1083 | |||
1084 | .cc.XMR::before { | ||
1085 | content: "\E10A"; | ||
1086 | } | ||
1087 | |||
1088 | .cc.XPM-alt::before { | ||
1089 | content: "\E10B"; | ||
1090 | } | ||
1091 | |||
1092 | .cc.XPM::before { | ||
1093 | content: "\E10C"; | ||
1094 | } | ||
1095 | |||
1096 | .cc.XRP-alt::before { | ||
1097 | content: "\E10D"; | ||
1098 | } | ||
1099 | |||
1100 | .cc.XRP::before { | ||
1101 | content: "\E10E"; | ||
1102 | } | ||
1103 | |||
1104 | .cc.XTZ-alt::before { | ||
1105 | content: "\E10F"; | ||
1106 | } | ||
1107 | |||
1108 | .cc.XTZ::before { | ||
1109 | content: "\E110"; | ||
1110 | } | ||
1111 | |||
1112 | .cc.XVG-alt::before { | ||
1113 | content: "\E111"; | ||
1114 | } | ||
1115 | |||
1116 | .cc.XVG::before { | ||
1117 | content: "\E112"; | ||
1118 | } | ||
1119 | |||
1120 | .cc.XZC-alt::before { | ||
1121 | content: "\E113"; | ||
1122 | } | ||
1123 | |||
1124 | .cc.XZC::before { | ||
1125 | content: "\E114"; | ||
1126 | } | ||
1127 | |||
1128 | .cc.YBC-alt::before { | ||
1129 | content: "\E115"; | ||
1130 | } | ||
1131 | |||
1132 | .cc.YBC::before { | ||
1133 | content: "\E116"; | ||
1134 | } | ||
1135 | |||
1136 | .cc.ZEC-alt::before { | ||
1137 | content: "\E117"; | ||
1138 | } | ||
1139 | |||
1140 | .cc.ZEC::before { | ||
1141 | content: "\E118"; | ||
1142 | } | ||
1143 | |||
1144 | .cc.ZEIT-alt::before { | ||
1145 | content: "\E119"; | ||
1146 | } | ||
1147 | |||
1148 | .cc.ZEIT::before { | ||
1149 | content: "\E11A"; | ||
1150 | } \ No newline at end of file | ||
diff --git a/cmd/web/static/cryptocoins.ttf b/cmd/web/static/cryptocoins.ttf new file mode 100644 index 0000000..b92c27d --- /dev/null +++ b/cmd/web/static/cryptocoins.ttf | |||
Binary files differ | |||
diff --git a/cmd/web/static/cryptocoins.woff b/cmd/web/static/cryptocoins.woff new file mode 100644 index 0000000..bf8eb08 --- /dev/null +++ b/cmd/web/static/cryptocoins.woff | |||
Binary files differ | |||
diff --git a/cmd/web/static/cryptocoins.woff2 b/cmd/web/static/cryptocoins.woff2 new file mode 100644 index 0000000..dcc195c --- /dev/null +++ b/cmd/web/static/cryptocoins.woff2 | |||
Binary files differ | |||
diff --git a/cmd/web/static/index.html b/cmd/web/static/index.html index 9a70074..a372517 100644 --- a/cmd/web/static/index.html +++ b/cmd/web/static/index.html | |||
@@ -11,6 +11,7 @@ | |||
11 | <title>Cryptoportfolio</title> | 11 | <title>Cryptoportfolio</title> |
12 | <link href='https://fonts.googleapis.com/css?family=Fira+Mono' rel='stylesheet' type='text/css'> | 12 | <link href='https://fonts.googleapis.com/css?family=Fira+Mono' rel='stylesheet' type='text/css'> |
13 | <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> | 13 | <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> |
14 | <link rel="stylesheet" type="text/css" href="/public/cryptocoins.css"/> | ||
14 | <link rel="stylesheet" type="text/css" href="/public/style.css"/> | 15 | <link rel="stylesheet" type="text/css" href="/public/style.css"/> |
15 | <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> | 16 | <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> |
16 | <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> | 17 | <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> |
diff --git a/cmd/web/static/style.css b/cmd/web/static/style.css index 12af379..bfa43f0 100644 --- a/cmd/web/static/style.css +++ b/cmd/web/static/style.css | |||
@@ -3,7 +3,6 @@ | |||
3 | body { | 3 | body { |
4 | font-family: 'Fira Mono', 'Helvetica Neue', Arial, Helvetica, sans-serif; | 4 | font-family: 'Fira Mono', 'Helvetica Neue', Arial, Helvetica, sans-serif; |
5 | background-color: rgb(246, 248, 251); | 5 | background-color: rgb(246, 248, 251); |
6 | text-align: center; | ||
7 | } | 6 | } |
8 | 7 | ||
9 | ul { | 8 | ul { |
@@ -25,6 +24,10 @@ a:focus { | |||
25 | color: inherit; | 24 | color: inherit; |
26 | } | 25 | } |
27 | 26 | ||
27 | i.cc { | ||
28 | font-size: 1.5em; | ||
29 | } | ||
30 | |||
28 | #logo { | 31 | #logo { |
29 | text-align: center; | 32 | text-align: center; |
30 | display: inline-block; | 33 | display: inline-block; |
@@ -93,8 +96,8 @@ a:focus { | |||
93 | background-color: rgb(250, 250, 250); | 96 | background-color: rgb(250, 250, 250); |
94 | box-shadow: 0 2px 6px 2px rgba(0,0,0,.05); | 97 | box-shadow: 0 2px 6px 2px rgba(0,0,0,.05); |
95 | border-radius: 4px; | 98 | border-radius: 4px; |
96 | } | 99 | margin-bottom: 10px; |
97 | 100 | margin-top: 10px; | |
98 | .box:hover { | 101 | padding-top: 10px; |
99 | box-shadow: 0 4px 15px 2px rgba(0,0,0,.20); | 102 | padding-bottom: 10px; |
100 | } | 103 | } \ No newline at end of file |
diff --git a/markets/poloniex.go b/markets/poloniex.go new file mode 100644 index 0000000..5e1ec64 --- /dev/null +++ b/markets/poloniex.go | |||
@@ -0,0 +1,177 @@ | |||
1 | package markets | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "strings" | ||
6 | |||
7 | "github.com/jloup/poloniex" | ||
8 | "github.com/jloup/utils" | ||
9 | "github.com/shopspring/decimal" | ||
10 | ) | ||
11 | |||
12 | var ( | ||
13 | ErrorFlagCounter utils.Counter = 0 | ||
14 | CurrencyPairNotInTicker = utils.InitFlag(&ErrorFlagCounter, "CurrencyPairNotInTicker") | ||
15 | InvalidCredentials = utils.InitFlag(&ErrorFlagCounter, "InvalidCredentials") | ||
16 | ) | ||
17 | |||
18 | func poloniexInvalidCredentialsError(err error) bool { | ||
19 | if err == nil { | ||
20 | return false | ||
21 | } | ||
22 | return strings.Contains(err.Error(), "Invalid API key/secret pair") | ||
23 | } | ||
24 | |||
25 | type CurrencyPair struct { | ||
26 | Name string | ||
27 | Rate decimal.Decimal | ||
28 | } | ||
29 | |||
30 | type Poloniex struct { | ||
31 | TickerCache map[string]CurrencyPair | ||
32 | |||
33 | publicClient *poloniex.Poloniex | ||
34 | updateTickerChan chan CurrencyPair | ||
35 | } | ||
36 | |||
37 | func NewPoloniex() *Poloniex { | ||
38 | client, _ := poloniex.NewClient("", "") | ||
39 | |||
40 | return &Poloniex{ | ||
41 | TickerCache: make(map[string]CurrencyPair), | ||
42 | updateTickerChan: nil, | ||
43 | publicClient: client, | ||
44 | } | ||
45 | } | ||
46 | |||
47 | func (p *Poloniex) GetBalance(apiKey, apiSecret string) (map[string]decimal.Decimal, error) { | ||
48 | client, _ := poloniex.NewClient(apiKey, apiSecret) | ||
49 | |||
50 | accounts, err := client.TradeReturnAvailableAccountBalances() | ||
51 | if poloniexInvalidCredentialsError(err) { | ||
52 | return nil, utils.Error{InvalidCredentials, "invalid poloniex credentials"} | ||
53 | } | ||
54 | |||
55 | if err != nil { | ||
56 | return nil, err | ||
57 | } | ||
58 | |||
59 | balances := make(map[string]decimal.Decimal) | ||
60 | for currency, balance := range accounts.Margin { | ||
61 | balances[currency] = balances[currency].Add(balance) | ||
62 | } | ||
63 | |||
64 | for currency, balance := range accounts.Exchange { | ||
65 | balances[currency] = balances[currency].Add(balance) | ||
66 | } | ||
67 | |||
68 | return balances, nil | ||
69 | } | ||
70 | |||
71 | func (p *Poloniex) ComputeAccountBalanceValue(account map[string]decimal.Decimal, baseCurrency string) (decimal.Decimal, error) { | ||
72 | var total decimal.Decimal | ||
73 | |||
74 | for currency, amount := range account { | ||
75 | pair, err := p.GetCurrencyPair(baseCurrency, currency) | ||
76 | if err != nil { | ||
77 | return decimal.Zero, err | ||
78 | } | ||
79 | |||
80 | total = total.Add(amount.Mul(pair.Rate)) | ||
81 | } | ||
82 | |||
83 | return total, nil | ||
84 | } | ||
85 | |||
86 | func (p *Poloniex) GetCurrencyPair(curr1, curr2 string) (CurrencyPair, error) { | ||
87 | pairName := fmt.Sprintf("%s_%s", curr1, curr2) | ||
88 | var err error | ||
89 | |||
90 | if curr1 == curr2 { | ||
91 | return CurrencyPair{pairName, decimal.NewFromFloat(1.0)}, nil | ||
92 | } | ||
93 | |||
94 | pair, ok := p.TickerCache[pairName] | ||
95 | if !ok { | ||
96 | pair, err = p.fetchTicker(curr1, curr2) | ||
97 | |||
98 | if utils.ErrIs(err, CurrencyPairNotInTicker) { | ||
99 | // try to invert an existing ticker. | ||
100 | pair, err = p.fetchTicker(curr2, curr1) | ||
101 | if err != nil { | ||
102 | return CurrencyPair{}, err | ||
103 | } | ||
104 | |||
105 | return CurrencyPair{pairName, decimal.NewFromFloat(1.0).Div(pair.Rate)}, nil | ||
106 | } | ||
107 | |||
108 | if err != nil { | ||
109 | return CurrencyPair{}, err | ||
110 | } | ||
111 | } | ||
112 | |||
113 | return pair, nil | ||
114 | } | ||
115 | |||
116 | func (p *Poloniex) fetchTicker(curr1, curr2 string) (CurrencyPair, error) { | ||
117 | tickers, err := p.publicClient.PubReturnTickers() | ||
118 | if err != nil { | ||
119 | return CurrencyPair{}, err | ||
120 | } | ||
121 | |||
122 | pairName := fmt.Sprintf("%s_%s", curr1, curr2) | ||
123 | |||
124 | if ticker, ok := tickers[pairName]; ok { | ||
125 | pair := CurrencyPair{Name: pairName, Rate: ticker.Last} | ||
126 | |||
127 | if p.updateTickerChan != nil { | ||
128 | p.updateTickerChan <- pair | ||
129 | } | ||
130 | |||
131 | return pair, nil | ||
132 | } | ||
133 | |||
134 | return CurrencyPair{}, utils.Error{CurrencyPairNotInTicker, fmt.Sprintf("%s_%s not in ticker", curr1, curr2)} | ||
135 | } | ||
136 | |||
137 | func (p *Poloniex) StartTicker() error { | ||
138 | stream, err := poloniex.NewWSClient() | ||
139 | if err != nil { | ||
140 | return err | ||
141 | } | ||
142 | |||
143 | err = stream.SubscribeTicker() | ||
144 | if err != nil { | ||
145 | return err | ||
146 | } | ||
147 | |||
148 | p.updateTickerChan = make(chan CurrencyPair) | ||
149 | |||
150 | for { | ||
151 | quit := false | ||
152 | select { | ||
153 | case data, ok := <-stream.Subs["ticker"]: | ||
154 | if !ok { | ||
155 | quit = true | ||
156 | } else { | ||
157 | ticker := data.(poloniex.WSTicker) | ||
158 | if ticker.CurrencyPair == "USDT_BTC" || true { | ||
159 | } | ||
160 | p.TickerCache[ticker.CurrencyPair] = CurrencyPair{Name: ticker.CurrencyPair, Rate: decimal.NewFromFloat(ticker.Last)} | ||
161 | } | ||
162 | |||
163 | case pair, ok := <-p.updateTickerChan: | ||
164 | if !ok { | ||
165 | quit = true | ||
166 | } else { | ||
167 | p.TickerCache[pair.Name] = pair | ||
168 | } | ||
169 | } | ||
170 | if quit { | ||
171 | p.updateTickerChan = nil | ||
172 | break | ||
173 | } | ||
174 | } | ||
175 | |||
176 | return nil | ||
177 | } | ||