aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjloup <jeanloup.jamet@gmail.com>2018-02-22 11:31:59 +0100
committerjloup <jeanloup.jamet@gmail.com>2018-02-22 11:31:59 +0100
commit2f91f20a8645339385ada602684f4957f20f4da4 (patch)
treea084cf293af15cc6ea6a1417edad51fcf0674947
parent3602fbf8412d69900d793a963c8e774f487f5e45 (diff)
downloadFront-2f91f20a8645339385ada602684f4957f20f4da4.tar.gz
Front-2f91f20a8645339385ada602684f4957f20f4da4.tar.zst
Front-2f91f20a8645339385ada602684f4957f20f4da4.zip
Poloniex connection.
-rw-r--r--Gopkg.lock22
-rw-r--r--Gopkg.toml8
-rw-r--r--api/const.go9
-rw-r--r--api/const_string.go4
-rw-r--r--api/external_services.go40
-rw-r--r--api/market_config.go76
-rw-r--r--api/markets.go29
-rw-r--r--api/routes.go11
-rw-r--r--cmd/web/Makefile19
-rw-r--r--cmd/web/js/api.js11
-rw-r--r--cmd/web/js/main.jsx41
-rw-r--r--cmd/web/js/otp.jsx6
-rw-r--r--cmd/web/js/poloniex.jsx176
-rw-r--r--cmd/web/js/signin.jsx4
-rw-r--r--cmd/web/js/signup.jsx4
-rw-r--r--cmd/web/static/cryptocoins.css1150
-rw-r--r--cmd/web/static/cryptocoins.ttfbin0 -> 96448 bytes
-rw-r--r--cmd/web/static/cryptocoins.woffbin0 -> 58768 bytes
-rw-r--r--cmd/web/static/cryptocoins.woff2bin0 -> 49976 bytes
-rw-r--r--cmd/web/static/index.html1
-rw-r--r--cmd/web/static/style.css15
-rw-r--r--markets/poloniex.go177
22 files changed, 1732 insertions, 71 deletions
diff --git a/Gopkg.lock b/Gopkg.lock
index 7f0f166..88c2eda 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -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
diff --git a/Gopkg.toml b/Gopkg.toml
index f4686b5..4feee4d 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -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"
6type Status uint32 6type Status uint32
7type ErrorCode uint32 7type ErrorCode uint32
8 8
9const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 10
10
9const ( 11const (
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
18const _ErrorCode_name = "BadRequestEmailExistsInternalErrorInvalidCredentialsInvalidEmailInvalidOtpInvalidPasswordNeedOtpValidationNotAuthorizedNotFoundOtpAlreadySetupOtpNotSetupUserNotConfirmed" 18const _ErrorCode_name = "BadRequestEmailExistsExternalServiceTimeoutInternalErrorInvalidCredentialsInvalidEmailInvalidMarketCredentialsInvalidOtpInvalidPasswordNeedOtpValidationNotAuthorizedNotFoundOtpAlreadySetupOtpNotSetupUserNotConfirmed"
19 19
20var _ErrorCode_index = [...]uint8{0, 10, 21, 34, 52, 64, 74, 89, 106, 119, 127, 142, 153, 169} 20var _ErrorCode_index = [...]uint8{0, 10, 21, 43, 56, 74, 86, 110, 120, 135, 152, 165, 173, 188, 199, 215}
21 21
22func (i ErrorCode) String() string { 22func (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 @@
1package api
2
3import (
4 "context"
5 "fmt"
6 "time"
7)
8
9// Use this to call external services. It will handle timeout and request cancellation gracefully.
10func 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
28var ErrorChan chan error
29
30func ErrorMonitoring() {
31 for {
32 err := <-ErrorChan
33 log.Errorf("error: %v", err)
34 }
35}
36
37func 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
3import ( 3import (
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
51type MarketBalanceQuery struct {
52 In struct {
53 User db.User
54 Market string
55 Currency string
56 }
57}
58
59func (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
72func (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
45type UpdateMarketConfigQuery struct { 118type 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 @@
1package api
2
3import (
4 "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets"
5)
6
7var Poloniex *markets.Poloniex
8
9func 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
19func 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
115func 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
114func UpdateMarketConfig(c *gin.Context) { 125func 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
6BUILD_DIR=build/js 6BUILD_DIR=build/js
7JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx 7JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx
8JS_SRC= cookies.js app.js api.js 8JS_SRC= cookies.js app.js api.js
9STATIC_FILES= index.html style.css cryptocoins.css cryptocoins.ttf cryptocoins.woff cryptocoins.woff2
9JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js)) 10JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js))
10JS_OBJS=$(addprefix $(BUILD_DIR)/,$(JS_SRC)) 11JS_OBJS=$(addprefix $(BUILD_DIR)/,$(JS_SRC))
11STATIC_BUILD_DIR=build/static 12STATIC_BUILD_DIR=build/static
@@ -16,18 +17,18 @@ install:
16 yarn --version 17 yarn --version
17 yarn install 18 yarn install
18 19
19static: js $(STATIC_BUILD_DIR)/index.html $(STATIC_BUILD_DIR)/style.css 20static: $(STATIC_BUILD_DIR) js $(addprefix $(STATIC_BUILD_DIR)/, $(STATIC_FILES))
20 21
21js: build/static/main.js 22js: 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
45build/webapp.tar.gz: $(STATIC_BUILD_DIR)/main.js $(STATIC_BUILD_DIR)/index.html $(STATIC_BUILD_DIR)/style.css 46build/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
48release: build/webapp.tar.gz 49release: $(STATIC_BUILD_DIR) build/webapp.tar.gz
49 50
50clean: 51clean:
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 @@
1var SignupForm = require('./signup.js').SignupForm; 1var SignupForm = require('./signup.js').SignupForm;
2var SigninForm = require('./signin.js').SigninForm; 2var SigninForm = require('./signin.js').SigninForm;
3var OtpEnrollForm = require('./otp.js').OtpEnrollForm; 3var OtpEnrollForm = require('./otp.js').OtpEnrollForm;
4var PoloniexForm = require('./poloniex.js').PoloniexForm; 4var PoloniexController = require('./poloniex.js').PoloniexController;
5var App = require('./app.js'); 5var App = require('./app.js');
6var Api = require('./api.js').Api; 6var Api = require('./api.js').Api;
7var cookies = require('./cookies.js'); 7var cookies = require('./cookies.js');
8 8
9var Logo = React.createClass({ 9var 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
51App.page('/me', true, function(context) { 53App.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
69App.page('/otp/setup', true, function(context) { 62App.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 @@
1var Api = require('./api.js').Api; 1var Api = require('./api.js').Api;
2var App = require('./app.js');
3var classNames = require('classnames'); 2var classNames = require('classnames');
4 3
5module.exports.PoloniexForm = React.createClass({ 4module.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
86var 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
138module.exports.PoloniexBalance = PoloniexBalance;
139
140var 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
185module.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 @@
3body { 3body {
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
9ul { 8ul {
@@ -25,6 +24,10 @@ a:focus {
25 color: inherit; 24 color: inherit;
26} 25}
27 26
27i.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 @@
1package markets
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/jloup/poloniex"
8 "github.com/jloup/utils"
9 "github.com/shopspring/decimal"
10)
11
12var (
13 ErrorFlagCounter utils.Counter = 0
14 CurrencyPairNotInTicker = utils.InitFlag(&ErrorFlagCounter, "CurrencyPairNotInTicker")
15 InvalidCredentials = utils.InitFlag(&ErrorFlagCounter, "InvalidCredentials")
16)
17
18func 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
25type CurrencyPair struct {
26 Name string
27 Rate decimal.Decimal
28}
29
30type Poloniex struct {
31 TickerCache map[string]CurrencyPair
32
33 publicClient *poloniex.Poloniex
34 updateTickerChan chan CurrencyPair
35}
36
37func 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
47func (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
71func (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
86func (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
116func (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
137func (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}