packages = ["."]
revision = "1c35d901db3da928c72a72d8458480cc9ade058f"
+[[projects]]
+ branch = "master"
+ name = "github.com/jloup/poloniex"
+ packages = ["."]
+ revision = "e75e6fd7991c1d71576ad97de73fc922f24a5fd2"
+
[[projects]]
branch = "master"
name = "github.com/jloup/utils"
revision = "b7b89250c468c06871d3837bee02e2d5c155ae19"
version = "v1.0.0"
+[[projects]]
+ branch = "master"
+ name = "github.com/shopspring/decimal"
+ packages = ["."]
+ revision = "e3482495ff4cba75613e4177ed79825c890058a9"
+
[[projects]]
name = "github.com/ugorji/go"
packages = ["codec"]
"blowfish",
"ssh/terminal"
]
- revision = "650f4a345ab4e5b245a3034b110ebc7299e68186"
+ revision = "432090b8f568c018896cd8a0fb0345872bbac6ce"
+
+[[projects]]
+ branch = "master"
+ name = "golang.org/x/net"
+ packages = ["websocket"]
+ revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb"
[[projects]]
branch = "master"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
- inputs-digest = "c9af022a586632799c6259f6c48eef8dad7080e36b96e8cb5cb905b316c4cb9b"
+ inputs-digest = "d3c9b3094ed174bcf1631e3a998a75d557c65c195d7a8fd5ca9912f71f334ce1"
solver-name = "gps-cdcl"
solver-version = 1
[prune]
go-tests = true
unused-packages = true
+
+[[constraint]]
+ branch = "master"
+ name = "github.com/jloup/poloniex"
+
+[[constraint]]
+ branch = "master"
+ name = "github.com/shopspring/decimal"
type Status uint32
type ErrorCode uint32
+const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 10
+
const (
OK Status = iota
NOK
BadRequest ErrorCode = iota + 1
EmailExists
+ ExternalServiceTimeout
InternalError
InvalidCredentials
InvalidEmail
+ InvalidMarketCredentials
InvalidOtp
InvalidPassword
NeedOtpValidation
}
switch code {
- case BadRequest, InvalidPassword, InvalidEmail:
+ case BadRequest, InvalidPassword, InvalidEmail, InvalidMarketCredentials:
return http.StatusBadRequest
case InvalidCredentials, InvalidOtp:
case NotFound:
return http.StatusNotFound
+
+ case ExternalServiceTimeout:
+ return http.StatusGatewayTimeout
}
return http.StatusInternalServerError
return _Status_name[_Status_index[i]:_Status_index[i+1]]
}
-const _ErrorCode_name = "BadRequestEmailExistsInternalErrorInvalidCredentialsInvalidEmailInvalidOtpInvalidPasswordNeedOtpValidationNotAuthorizedNotFoundOtpAlreadySetupOtpNotSetupUserNotConfirmed"
+const _ErrorCode_name = "BadRequestEmailExistsExternalServiceTimeoutInternalErrorInvalidCredentialsInvalidEmailInvalidMarketCredentialsInvalidOtpInvalidPasswordNeedOtpValidationNotAuthorizedNotFoundOtpAlreadySetupOtpNotSetupUserNotConfirmed"
-var _ErrorCode_index = [...]uint8{0, 10, 21, 34, 52, 64, 74, 89, 106, 119, 127, 142, 153, 169}
+var _ErrorCode_index = [...]uint8{0, 10, 21, 43, 56, 74, 86, 110, 120, 135, 152, 165, 173, 188, 199, 215}
func (i ErrorCode) String() string {
i -= 3
--- /dev/null
+package api
+
+import (
+ "context"
+ "fmt"
+ "time"
+)
+
+// Use this to call external services. It will handle timeout and request cancellation gracefully.
+func CallExternalService(tag string, timeout time.Duration, routine func() *Error) *Error {
+ routineDone := make(chan *Error)
+
+ go func() {
+ routineDone <- routine()
+ }()
+
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+
+ select {
+ case err := <-routineDone:
+ return err
+ case <-ctx.Done():
+ return &Error{ExternalServiceTimeout, "external service timeout", fmt.Errorf("'%v' routine timeouted", tag)}
+ }
+}
+
+var ErrorChan chan error
+
+func ErrorMonitoring() {
+ for {
+ err := <-ErrorChan
+ log.Errorf("error: %v", err)
+ }
+}
+
+func init() {
+ ErrorChan = make(chan error)
+ go ErrorMonitoring()
+}
import (
"fmt"
+ "strings"
+ "time"
+ "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets"
+
+ "github.com/jloup/utils"
+ "github.com/shopspring/decimal"
"immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db"
)
return config.Config, nil
}
+type MarketBalanceQuery struct {
+ In struct {
+ User db.User
+ Market string
+ Currency string
+ }
+}
+
+func (q MarketBalanceQuery) ValidateParams() *Error {
+ if q.In.Market != "poloniex" {
+ return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)}
+ }
+
+ // TODO: we should request market for available currencies.
+ if q.In.Currency != "BTC" && q.In.Currency != "USDT" && q.In.Currency != "ETH" {
+ return &Error{BadRequest, "invalid currency, accept [BTC, USDT, ETH]", fmt.Errorf("'%v' is not a valid currency", q.In.Currency)}
+ }
+
+ return nil
+}
+
+func (q MarketBalanceQuery) Run() (interface{}, *Error) {
+ config, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market)
+ if err != nil {
+ return nil, NewInternalError(err)
+ }
+
+ if config.Config["key"] == "" || config.Config["secret"] == "" {
+ return nil, &Error{BadRequest, "your credentials for this market are not setup", fmt.Errorf("'%v' credentials are not setup", q.In.Market)}
+ }
+
+ result := struct {
+ Value decimal.Decimal `json:"value"`
+ ValueCurrency string `json:"valueCurrency"`
+ Balance map[string]decimal.Decimal `json:"balance"`
+ }{}
+
+ resultErr := CallExternalService(fmt.Sprintf("'%s' GetBalanceValue", q.In.Market), EXTERNAL_SERVICE_TIMEOUT_SECONDS*time.Second, func() *Error {
+ balance, err := Poloniex.GetBalance(config.Config["key"], config.Config["secret"])
+
+ if utils.ErrIs(err, markets.InvalidCredentials) {
+ return &Error{InvalidMarketCredentials, "wrong market credentials", fmt.Errorf("wrong '%v' market credentials", q.In.Market)}
+ }
+
+ if err != nil {
+ return NewInternalError(err)
+ }
+
+ value, err := Poloniex.ComputeAccountBalanceValue(balance, q.In.Currency)
+ if err != nil {
+ return NewInternalError(err)
+ }
+
+ result.Balance = balance
+ result.ValueCurrency = q.In.Currency
+ result.Value = value.Round(8)
+
+ return nil
+ })
+
+ if resultErr != nil {
+ return nil, resultErr
+ }
+
+ return &result, nil
+}
+
type UpdateMarketConfigQuery struct {
In struct {
User db.User
return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)}
}
+ q.In.Secret = strings.TrimSpace(q.In.Secret)
+ q.In.Key = strings.TrimSpace(q.In.Key)
+
return nil
}
--- /dev/null
+package api
+
+import (
+ "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets"
+)
+
+var Poloniex *markets.Poloniex
+
+func OpenMarketsConnection() error {
+ for {
+ err := Poloniex.StartTicker()
+ if err != nil {
+ return err
+ }
+ log.Warn("connection to poloniex stream ended, restarting it...")
+ }
+}
+
+func init() {
+ Poloniex = markets.NewPoloniex()
+
+ // We open markets connections in the background as it can take time.
+ go func() {
+ err := OpenMarketsConnection()
+ if err != nil {
+ ErrorChan <- err
+ }
+ }()
+}
[]Route{
{"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"},
{"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"},
+ {"GET", []gin.HandlerFunc{GetMarketBalance}, "/:name/balance"},
},
},
}
RunQuery(query, c)
}
+func GetMarketBalance(c *gin.Context) {
+ query := &MarketBalanceQuery{}
+
+ query.In.User = GetUser(c)
+ query.In.Market = c.Param("name")
+ query.In.Currency = c.Query("currency")
+
+ RunQuery(query, c)
+}
+
func UpdateMarketConfig(c *gin.Context) {
query := &UpdateMarketConfigQuery{}
BUILD_DIR=build/js
JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx
JS_SRC= cookies.js app.js api.js
+STATIC_FILES= index.html style.css cryptocoins.css cryptocoins.ttf cryptocoins.woff cryptocoins.woff2
JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js))
JS_OBJS=$(addprefix $(BUILD_DIR)/,$(JS_SRC))
STATIC_BUILD_DIR=build/static
yarn --version
yarn install
-static: js $(STATIC_BUILD_DIR)/index.html $(STATIC_BUILD_DIR)/style.css
+static: $(STATIC_BUILD_DIR) js $(addprefix $(STATIC_BUILD_DIR)/, $(STATIC_FILES))
js: build/static/main.js
-$(STATIC_BUILD_DIR)/index.html: static/index.html
- cp static/index.html $@
+$(STATIC_BUILD_DIR)/%: static/%
+ cp $< $@
-$(STATIC_BUILD_DIR)/style.css: static/style.css
- cp static/style.css $@
+$(STATIC_BUILD_DIR):
+ mkdir -p $(BUILD_DIR)
+ mkdir -p $@
$(BUILD_DIR)/%.js: $(SRC_DIR)/%.jsx
- mkdir -p $(@D)
jscs --fix $<
babel $< -o $@
jshint $@
-t [ debowerify ] \
$(BUILD_DIR)/main.js -o $@
-build/webapp.tar.gz: $(STATIC_BUILD_DIR)/main.js $(STATIC_BUILD_DIR)/index.html $(STATIC_BUILD_DIR)/style.css
+build/webapp.tar.gz: $(addprefix $(STATIC_BUILD_DIR)/, $(STATIC_FILES)) build/static/main.js
tar czf $@ --directory=$(dir $<) $(notdir $^)
-release: build/webapp.tar.gz
+release: $(STATIC_BUILD_DIR) build/webapp.tar.gz
clean:
rm -rf build
- rm -rf node_modules
\ No newline at end of file
+ rm -rf node_modules
return '/market/' + params.name;
}
},
+ 'MARKET_BALANCE': {
+ 'type': 'GET',
+ 'auth': true,
+ 'parameters': [
+ {'name': 'name', 'mandatory': true, 'inquery': false},
+ {'name': 'currency', 'mandatory': true, 'inquery': true},
+ ],
+ 'buildUrl': function(params) {
+ return '/market/' + params.name + '/balance';
+ }
+ },
'UPDATE_MARKET': {
'type': 'POST',
'auth': true,
-var SignupForm = require('./signup.js').SignupForm;
-var SigninForm = require('./signin.js').SigninForm;
-var OtpEnrollForm = require('./otp.js').OtpEnrollForm;
-var PoloniexForm = require('./poloniex.js').PoloniexForm;
-var App = require('./app.js');
-var Api = require('./api.js').Api;
-var cookies = require('./cookies.js');
+var SignupForm = require('./signup.js').SignupForm;
+var SigninForm = require('./signin.js').SigninForm;
+var OtpEnrollForm = require('./otp.js').OtpEnrollForm;
+var PoloniexController = require('./poloniex.js').PoloniexController;
+var App = require('./app.js');
+var Api = require('./api.js').Api;
+var cookies = require('./cookies.js');
var Logo = React.createClass({
render: function() {
- return (<div id='logo'>
- <a href='/'>Cryptoportfolio</a>
+ return (<div className='row'>
+ <div id='logo' className='offset-4 col-4'>
+ <a href='/'>Cryptoportfolio</a>
+ </div>
</div>);
}
});
});
App.page('/me', true, function(context) {
- Api.Call('MARKET', {'name': 'poloniex'}, function(err, status, data) {
- if (err) {
- console.error(err, data);
- return;
- }
-
- App.mount(
- <div>
- <Logo />
- <p>Poloniex</p>
- <PoloniexForm apiKey={data.key} apiSecret={data.secret}/>
- </div>
- );
-
- }.bind(this));
+ App.mount(
+ <div>
+ <Logo />
+ <PoloniexController/>
+ </div>
+ );
});
App.page('/otp/setup', true, function(context) {
);
}
return (
- <div className='row otp-enroll justify-content-center'>
- <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'>
+ <div className='row otp-enroll'>
+ <div className='offset-4 col-4 col-xs-offset-1 col-xs-10 text-center'>
{qrCode}
<div className='row justify-content-center'>
<form role='form' onSubmit={this.handleSubmit}>
- <input className='form-control' type='pass' placeholder='pass' onChange={this.handlePassChange} />
+ <input className='form-control' type='pass' placeholder='code' onChange={this.handlePassChange} />
<input className='form-control submit' type='submit' value='Validate' />
<div className={cName} ref='message'>{this.state.msg}</div>
</form>
var Api = require('./api.js').Api;
-var App = require('./app.js');
var classNames = require('classnames');
-module.exports.PoloniexForm = React.createClass({
+module.exports.PoloniexController = React.createClass({
getInitialState: function() {
- return {'hideMsg': true, 'msg': '', 'msgOk': false, 'apiSecret': this.props.apiSecret, 'apiKey': this.props.apiKey};
+ return {'apiKey': '', 'apiSecret': '', 'flag': 'loading', 'valueCurrency': null, 'balanceValue': null, 'balance': null};
},
- handleSubmit: function(e) {
+ handleCredentialsChange: function(key, secret) {
+ this.setState({'apiKey': key, 'apiSecret': secret});
+ },
+ handleCredentialsSubmit: function() {
+ if (!this.state.apiKey || !this.state.apiSecret) {
+ return;
+ }
Api.Call('UPDATE_MARKET', {'key': this.state.apiKey, 'secret': this.state.apiSecret, 'name': 'poloniex'}, function(err, status, data) {
if (err) {
console.error(err, data);
- this.displayMessage(App.errorCodeToMessage(err.code), false);
return;
}
- this.displayMessage('OK', true);
+ this.setState({'flag': 'loading', 'valueCurrency': null, 'balanceValue': null, 'balance': null});
+ this.loadBalance();
+ }.bind(this));
+ },
+ loadBalance: function() {
+ Api.Call('MARKET_BALANCE', {'name': 'poloniex', 'currency': 'BTC'}, function(err, status, data) {
+ if (err) {
+ console.error(err, data);
+ if (err.code === 'invalid_market_credentials') {
+ this.setState({'flag': 'invalidCredentials', 'valueCurrency': null, 'balanceValue': null, 'balance': null});
+ }
+ return;
+ }
+ this.setState({'flag': 'ok', 'valueCurrency': data.valueCurrency, 'balanceValue': data.value, 'balance': data.balance});
}.bind(this));
+ },
+ componentDidMount: function() {
+ Api.Call('MARKET', {'name': 'poloniex'}, function(err, status, data) {
+ if (err) {
+ console.error(err, data);
+ return;
+ }
+
+ var flag = this.state.flag;
+ if (!data.key || !data.secret) {
+ flag = 'emptyCredentials';
+ } else {
+ this.loadBalance();
+ }
+
+ this.setState({'apiKey': data.key, 'apiSecret': data.secret, 'flag': flag});
+ }.bind(this));
+ },
+ render: function() {
+ var displayText = null;
+ switch (this.state.flag) {
+ case 'loading':
+ displayText = 'Loading data from poloniex...';
+ break;
+ case 'invalidCredentials':
+ displayText = 'Invalid poloniex credentials';
+ break;
+ case 'emptyCredentials':
+ displayText = 'Please provide poloniex credentials';
+ break;
+ default:
+ displayText = null;
+ }
+ return (
+ <div>
+ <PoloniexBalance balanceCurrency={this.state.valueCurrency}
+ balanceValue={this.state.balanceValue}
+ balance={this.state.balance}
+ displayText={displayText}/>
+ <PoloniexCredentialsForm onLoadCredentials={this.onLoadCredentials}
+ onCredentialsSubmit={this.handleCredentialsSubmit}
+ onCredentialsChange={this.handleCredentialsChange}
+ apiSecret={this.state.apiSecret}
+ apiKey={this.state.apiKey}/>
+ </div>
+ );
+ }
+});
+
+var PoloniexBalance = React.createClass({
+ getInitialState: function() {
+ return {'hideMsg': true, 'msg': '', 'msgOk': false};
+ },
+ render: function() {
+ var dashboard = null;
+
+ if (this.props.balanceValue !== null) {
+
+ var balance = Object.keys(this.props.balance).map(function(currency) {
+ return <div key={currency}><i className={classNames('cc', currency)}></i> {this.props.balance[currency]}</div>;
+ }.bind(this));
+
+ dashboard = (
+ <div className='row'>
+ <div className='col-4 align-self-center h-100'>
+ <div>
+ {balance}
+ </div>
+ </div>
+ <div className='offset-1 col-7 h-100 align-self-center'>
+ <div className='text-center'>
+ Balance ({this.props.balanceCurrency}): <span>{this.props.balanceValue} <i className={classNames('cc', this.props.balanceCurrency)}></i></span>
+ </div>
+ </div>
+ </div>
+ );
+ } else {
+ dashboard = (
+ <div className='row'>
+ <div className='col-12 text-center'>
+ <span>{this.props.displayText}</span>
+ </div>
+ </div>
+
+ );
+ }
+
+ return (
+ <div className='row'>
+ <div className='box offset-2 col-8'>
+ <div className='row'>
+ <div className='col-4'>Portfolio</div>
+ </div>
+ <hr/>
+ {dashboard}
+ </div>
+ </div>
+ );
+ }
+});
+
+module.exports.PoloniexBalance = PoloniexBalance;
+
+var PoloniexCredentialsForm = React.createClass({
+ getInitialState: function() {
+ return {'hideMsg': true, 'msg': '', 'editMode': false, 'msgOk': false};
+ },
+ handleSubmit: function(e) {
+ this.props.onCredentialsSubmit();
+ this.setState({'editMode': false});
e.preventDefault();
},
handleApiKeyChange: function(event) {
- this.setState({'apiKey': event.target.value});
+ this.props.onCredentialsChange(event.target.value, this.props.apiSecret);
},
handleApiSecretChange: function(event) {
- this.setState({'apiSecret': event.target.value});
+ this.props.onCredentialsChange(this.props.apiKey, event.target.value);
},
- hideMessage: function() {
- this.setState({'hideMsg': true});
- },
- displayMessage: function(msg, ok) {
- this.setState({'msg': msg, 'msgOk': ok, 'hideMsg': false});
+ onEditClick: function() {
+ this.setState({'editMode': true});
},
render: function() {
- var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk});
+ var submitType = (this.state.editMode === true) ? 'submit' : 'hidden';
+ var buttonDisplay = (this.state.editMode === true) ? 'none' : 'inline';
+ var secretDisplayed = (this.state.editMode === true) ? this.props.apiSecret : 'XXXXXXX';
+ var keyDisplayed = (this.state.editMode === true) ? this.props.apiKey : 'XXXXXXX';
+
return (
- <div className='row justify-content-center api-credentials-form'>
- <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'>
+ <div className='row api-credentials-form'>
+ <div className='offset-3 col-6 box'>
+ <span className='text-center'>Poloniex credentials</span>
+ <hr/>
<form role='form' onSubmit={this.handleSubmit}>
- <input className='form-control' type='text' placeholder='apiKey' value={this.state.apiKey} onChange={this.handleApiKeyChange} />
- <input className='form-control' type='text' placeholder='apiSecret' value={this.state.apiSecret} onChange={this.handleApiSecretChange} />
- <input className='form-control submit' type='submit' value='Save' />
- <div className={cName} ref='message'>{this.state.msg}</div>
+ <label className='w-100'>Key:
+ <input className='form-control' type='text' placeholder='key' value={keyDisplayed} onChange={this.handleApiKeyChange} disabled={!this.state.editMode}/>
+ </label>
+ <label className='w-100'>Secret:
+ <input className='form-control' type='text' placeholder='secret' value={secretDisplayed} onChange={this.handleApiSecretChange} disabled={!this.state.editMode}/>
+ </label>
+ <input className='form-control submit' type={submitType} value='Save' />
+ <button className='form-control submit' style={{display: buttonDisplay}} onSubmit={null} onClick={this.onEditClick} type='button'>Show/Edit</button>
</form>
</div>
</div>
);
}
});
+
+module.exports.PoloniexCredentialsForm = PoloniexCredentialsForm;
render: function() {
var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk});
return (
- <div className='row justify-content-center sign-in'>
- <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'>
+ <div className='row sign-in'>
+ <div className='offset-4 col-4 col-xs-offset-1 col-xs-10 text-center'>
<form role='form' onSubmit={this.handleSubmit}>
<input className='form-control' type='email' placeholder='email' onChange={this.handleEmailChange} />
<input className='form-control' type='password' placeholder='password' onChange={this.handlePasswordChange} />
render: function() {
var cName = classNames('form-message', {'hidden': this.state.hideMsg, 'message-ok': this.state.msgOk});
return (
- <div className='row justify-content-center sign-in'>
- <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'>
+ <div className='row sign-in'>
+ <div className='offset-4 col-4 col-xs-offset-1 col-xs-10 text-center'>
<form role='form' onSubmit={this.handleSubmit}>
<input className='form-control' type='email' placeholder='email' onChange={this.handleEmailChange} />
<input className='form-control' type='password' placeholder='password' onChange={this.handlePasswordChange} />
--- /dev/null
+/*! Cryptocoins - cryptocurrency icon font | https://github.com/allienworks/cryptocoins */
+
+@font-face {
+ font-family: "cryptocoins";
+ src: url('cryptocoins.woff2') format('woff2'),
+ url('cryptocoins.woff') format('woff'),
+ url('cryptocoins.ttf') format('truetype');
+}
+
+/* .cc:before { */
+.cc::before {
+ font-family: "cryptocoins";
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: normal;
+ /* speak: none; only necessary if not using the private unicode range (firstGlyph option) */
+ text-decoration: none;
+ text-transform: none;
+}
+
+
+.cc.ADA-alt::before {
+ content: "\E001";
+}
+
+.cc.ADA::before {
+ content: "\E002";
+}
+
+.cc.ADC-alt::before {
+ content: "\E003";
+}
+
+.cc.ADC::before {
+ content: "\E004";
+}
+
+.cc.AEON-alt::before {
+ content: "\E005";
+}
+
+.cc.AEON::before {
+ content: "\E006";
+}
+
+.cc.AMP-alt::before {
+ content: "\E007";
+}
+
+.cc.AMP::before {
+ content: "\E008";
+}
+
+.cc.ANC-alt::before {
+ content: "\E009";
+}
+
+.cc.ANC::before {
+ content: "\E00A";
+}
+
+.cc.ARCH-alt::before {
+ content: "\E00B";
+}
+
+.cc.ARCH::before {
+ content: "\E00C";
+}
+
+.cc.ARDR-alt::before {
+ content: "\E00D";
+}
+
+.cc.ARDR::before {
+ content: "\E00E";
+}
+
+.cc.ARK-alt::before {
+ content: "\E00F";
+}
+
+.cc.ARK::before {
+ content: "\E010";
+}
+
+.cc.AUR-alt::before {
+ content: "\E011";
+}
+
+.cc.AUR::before {
+ content: "\E012";
+}
+
+.cc.BANX-alt::before {
+ content: "\E013";
+}
+
+.cc.BANX::before {
+ content: "\E014";
+}
+
+.cc.BAT-alt::before {
+ content: "\E015";
+}
+
+.cc.BAT::before {
+ content: "\E016";
+}
+
+.cc.BAY-alt::before {
+ content: "\E017";
+}
+
+.cc.BAY::before {
+ content: "\E018";
+}
+
+.cc.BC-alt::before {
+ content: "\E019";
+}
+
+.cc.BC::before {
+ content: "\E01A";
+}
+
+.cc.BCH-alt::before {
+ content: "\E01B";
+}
+
+.cc.BCH::before {
+ content: "\E01C";
+}
+
+.cc.BCN-alt::before {
+ content: "\E01D";
+}
+
+.cc.BCN::before {
+ content: "\E01E";
+}
+
+.cc.BFT-alt::before {
+ content: "\E01F";
+}
+
+.cc.BFT::before {
+ content: "\E020";
+}
+
+.cc.BRK-alt::before {
+ content: "\E021";
+}
+
+.cc.BRK::before {
+ content: "\E022";
+}
+
+.cc.BRX-alt::before {
+ content: "\E023";
+}
+
+.cc.BRX::before {
+ content: "\E024";
+}
+
+.cc.BSD-alt::before {
+ content: "\E025";
+}
+
+.cc.BSD::before {
+ content: "\E026";
+}
+
+.cc.BTA::before {
+ content: "\E027";
+}
+
+.cc.BTC-alt::before {
+ content: "\E028";
+}
+
+.cc.BTC::before {
+ content: "\E029";
+}
+
+.cc.BTCD-alt::before {
+ content: "\E02A";
+}
+
+.cc.BTCD::before {
+ content: "\E02B";
+}
+
+.cc.BTM-alt::before {
+ content: "\E02C";
+}
+
+.cc.BTM::before {
+ content: "\E02D";
+}
+
+.cc.BTS-alt::before {
+ content: "\E02E";
+}
+
+.cc.BTS::before {
+ content: "\E02F";
+}
+
+.cc.CLAM-alt::before {
+ content: "\E030";
+}
+
+.cc.CLAM::before {
+ content: "\E031";
+}
+
+.cc.CLOAK-alt::before {
+ content: "\E032";
+}
+
+.cc.CLOAK::before {
+ content: "\E033";
+}
+
+.cc.DAO-alt::before {
+ content: "\E034";
+}
+
+.cc.DAO::before {
+ content: "\E035";
+}
+
+.cc.DASH-alt::before {
+ content: "\E036";
+}
+
+.cc.DASH::before {
+ content: "\E037";
+}
+
+.cc.DCR-alt::before {
+ content: "\E038";
+}
+
+.cc.DCR::before {
+ content: "\E039";
+}
+
+.cc.DCT-alt::before {
+ content: "\E03A";
+}
+
+.cc.DCT::before {
+ content: "\E03B";
+}
+
+.cc.DGB-alt::before {
+ content: "\E03C";
+}
+
+.cc.DGB::before {
+ content: "\E03D";
+}
+
+.cc.DGD::before {
+ content: "\E03E";
+}
+
+.cc.DGX::before {
+ content: "\E03F";
+}
+
+.cc.DMD-alt::before {
+ content: "\E040";
+}
+
+.cc.DMD::before {
+ content: "\E041";
+}
+
+.cc.DOGE-alt::before {
+ content: "\E042";
+}
+
+.cc.DOGE::before {
+ content: "\E043";
+}
+
+.cc.EMC-alt::before {
+ content: "\E044";
+}
+
+.cc.EMC::before {
+ content: "\E045";
+}
+
+.cc.EOS-alt::before {
+ content: "\E046";
+}
+
+.cc.EOS::before {
+ content: "\E047";
+}
+
+.cc.ERC-alt::before {
+ content: "\E048";
+}
+
+.cc.ERC::before {
+ content: "\E049";
+}
+
+.cc.ETC-alt::before {
+ content: "\E04A";
+}
+
+.cc.ETC::before {
+ content: "\E04B";
+}
+
+.cc.ETH-alt::before {
+ content: "\E04C";
+}
+
+.cc.ETH::before {
+ content: "\E04D";
+}
+
+.cc.FC2-alt::before {
+ content: "\E04E";
+}
+
+.cc.FC2::before {
+ content: "\E04F";
+}
+
+.cc.FCT-alt::before {
+ content: "\E050";
+}
+
+.cc.FCT::before {
+ content: "\E051";
+}
+
+.cc.FLO-alt::before {
+ content: "\E052";
+}
+
+.cc.FLO::before {
+ content: "\E053";
+}
+
+.cc.FRK-alt::before {
+ content: "\E054";
+}
+
+.cc.FRK::before {
+ content: "\E055";
+}
+
+.cc.FTC-alt::before {
+ content: "\E056";
+}
+
+.cc.FTC::before {
+ content: "\E057";
+}
+
+.cc.GAME-alt::before {
+ content: "\E058";
+}
+
+.cc.GAME::before {
+ content: "\E059";
+}
+
+.cc.GBYTE-alt::before {
+ content: "\E05A";
+}
+
+.cc.GBYTE::before {
+ content: "\E05B";
+}
+
+.cc.GDC-alt::before {
+ content: "\E05C";
+}
+
+.cc.GDC::before {
+ content: "\E05D";
+}
+
+.cc.GEMZ-alt::before {
+ content: "\E05E";
+}
+
+.cc.GEMZ::before {
+ content: "\E05F";
+}
+
+.cc.GLD-alt::before {
+ content: "\E060";
+}
+
+.cc.GLD::before {
+ content: "\E061";
+}
+
+.cc.GNO-alt::before {
+ content: "\E062";
+}
+
+.cc.GNO::before {
+ content: "\E063";
+}
+
+.cc.GNT-alt::before {
+ content: "\E064";
+}
+
+.cc.GNT::before {
+ content: "\E065";
+}
+
+.cc.GOLOS-alt::before {
+ content: "\E066";
+}
+
+.cc.GOLOS::before {
+ content: "\E067";
+}
+
+.cc.GRC-alt::before {
+ content: "\E068";
+}
+
+.cc.GRC::before {
+ content: "\E069";
+}
+
+.cc.GRS::before {
+ content: "\E06A";
+}
+
+.cc.HEAT-alt::before {
+ content: "\E06B";
+}
+
+.cc.HEAT::before {
+ content: "\E06C";
+}
+
+.cc.ICN-alt::before {
+ content: "\E06D";
+}
+
+.cc.ICN::before {
+ content: "\E06E";
+}
+
+.cc.IFC-alt::before {
+ content: "\E06F";
+}
+
+.cc.IFC::before {
+ content: "\E070";
+}
+
+.cc.INCNT-alt::before {
+ content: "\E071";
+}
+
+.cc.INCNT::before {
+ content: "\E072";
+}
+
+.cc.IOC-alt::before {
+ content: "\E073";
+}
+
+.cc.IOC::before {
+ content: "\E074";
+}
+
+.cc.IOTA-alt::before {
+ content: "\E075";
+}
+
+.cc.IOTA::before {
+ content: "\E076";
+}
+
+.cc.JBS-alt::before {
+ content: "\E077";
+}
+
+.cc.JBS::before {
+ content: "\E078";
+}
+
+.cc.KMD-alt::before {
+ content: "\E079";
+}
+
+.cc.KMD::before {
+ content: "\E07A";
+}
+
+.cc.KOBO::before {
+ content: "\E07B";
+}
+
+.cc.KORE-alt::before {
+ content: "\E07C";
+}
+
+.cc.KORE::before {
+ content: "\E07D";
+}
+
+.cc.LBC-alt::before {
+ content: "\E07E";
+}
+
+.cc.LBC::before {
+ content: "\E07F";
+}
+
+.cc.LDOGE-alt::before {
+ content: "\E080";
+}
+
+.cc.LDOGE::before {
+ content: "\E081";
+}
+
+.cc.LSK-alt::before {
+ content: "\E082";
+}
+
+.cc.LSK::before {
+ content: "\E083";
+}
+
+.cc.LTC-alt::before {
+ content: "\E084";
+}
+
+.cc.LTC::before {
+ content: "\E085";
+}
+
+.cc.MAID-alt::before {
+ content: "\E086";
+}
+
+.cc.MAID::before {
+ content: "\E087";
+}
+
+.cc.MCO-alt::before {
+ content: "\E088";
+}
+
+.cc.MCO::before {
+ content: "\E089";
+}
+
+.cc.MINT-alt::before {
+ content: "\E08A";
+}
+
+.cc.MINT::before {
+ content: "\E08B";
+}
+
+.cc.MONA-alt::before {
+ content: "\E08C";
+}
+
+.cc.MONA::before {
+ content: "\E08D";
+}
+
+.cc.MRC::before {
+ content: "\E08E";
+}
+
+.cc.MSC-alt::before {
+ content: "\E08F";
+}
+
+.cc.MSC::before {
+ content: "\E090";
+}
+
+.cc.MTR-alt::before {
+ content: "\E091";
+}
+
+.cc.MTR::before {
+ content: "\E092";
+}
+
+.cc.MUE-alt::before {
+ content: "\E093";
+}
+
+.cc.MUE::before {
+ content: "\E094";
+}
+
+.cc.NBT::before {
+ content: "\E095";
+}
+
+.cc.NEO-alt::before {
+ content: "\E096";
+}
+
+.cc.NEO::before {
+ content: "\E097";
+}
+
+.cc.NEOS-alt::before {
+ content: "\E098";
+}
+
+.cc.NEOS::before {
+ content: "\E099";
+}
+
+.cc.NEU-alt::before {
+ content: "\E09A";
+}
+
+.cc.NEU::before {
+ content: "\E09B";
+}
+
+.cc.NLG-alt::before {
+ content: "\E09C";
+}
+
+.cc.NLG::before {
+ content: "\E09D";
+}
+
+.cc.NMC-alt::before {
+ content: "\E09E";
+}
+
+.cc.NMC::before {
+ content: "\E09F";
+}
+
+.cc.NOTE-alt::before {
+ content: "\E0A0";
+}
+
+.cc.NOTE::before {
+ content: "\E0A1";
+}
+
+.cc.NVC-alt::before {
+ content: "\E0A2";
+}
+
+.cc.NVC::before {
+ content: "\E0A3";
+}
+
+.cc.NXT-alt::before {
+ content: "\E0A4";
+}
+
+.cc.NXT::before {
+ content: "\E0A5";
+}
+
+.cc.OK-alt::before {
+ content: "\E0A6";
+}
+
+.cc.OK::before {
+ content: "\E0A7";
+}
+
+.cc.OMG-alt::before {
+ content: "\E0A8";
+}
+
+.cc.OMG::before {
+ content: "\E0A9";
+}
+
+.cc.OMNI-alt::before {
+ content: "\E0AA";
+}
+
+.cc.OMNI::before {
+ content: "\E0AB";
+}
+
+.cc.OPAL-alt::before {
+ content: "\E0AC";
+}
+
+.cc.OPAL::before {
+ content: "\E0AD";
+}
+
+.cc.PART-alt::before {
+ content: "\E0AE";
+}
+
+.cc.PART::before {
+ content: "\E0AF";
+}
+
+.cc.PIGGY-alt::before {
+ content: "\E0B0";
+}
+
+.cc.PIGGY::before {
+ content: "\E0B1";
+}
+
+.cc.PINK-alt::before {
+ content: "\E0B2";
+}
+
+.cc.PINK::before {
+ content: "\E0B3";
+}
+
+.cc.PIVX-alt::before {
+ content: "\E0B4";
+}
+
+.cc.PIVX::before {
+ content: "\E0B5";
+}
+
+.cc.POT-alt::before {
+ content: "\E0B6";
+}
+
+.cc.POT::before {
+ content: "\E0B7";
+}
+
+.cc.PPC-alt::before {
+ content: "\E0B8";
+}
+
+.cc.PPC::before {
+ content: "\E0B9";
+}
+
+.cc.QRK-alt::before {
+ content: "\E0BA";
+}
+
+.cc.QRK::before {
+ content: "\E0BB";
+}
+
+.cc.QTUM-alt::before {
+ content: "\E0BC";
+}
+
+.cc.QTUM::before {
+ content: "\E0BD";
+}
+
+.cc.RADS-alt::before {
+ content: "\E0BE";
+}
+
+.cc.RADS::before {
+ content: "\E0BF";
+}
+
+.cc.RBIES-alt::before {
+ content: "\E0C0";
+}
+
+.cc.RBIES::before {
+ content: "\E0C1";
+}
+
+.cc.RBT-alt::before {
+ content: "\E0C2";
+}
+
+.cc.RBT::before {
+ content: "\E0C3";
+}
+
+.cc.RBY-alt::before {
+ content: "\E0C4";
+}
+
+.cc.RBY::before {
+ content: "\E0C5";
+}
+
+.cc.RDD-alt::before {
+ content: "\E0C6";
+}
+
+.cc.RDD::before {
+ content: "\E0C7";
+}
+
+.cc.REP-alt::before {
+ content: "\E0C8";
+}
+
+.cc.REP::before {
+ content: "\E0C9";
+}
+
+.cc.RISE-alt::before {
+ content: "\E0CA";
+}
+
+.cc.RISE::before {
+ content: "\E0CB";
+}
+
+.cc.SALT-alt::before {
+ content: "\E0CC";
+}
+
+.cc.SALT::before {
+ content: "\E0CD";
+}
+
+.cc.SAR-alt::before {
+ content: "\E0CE";
+}
+
+.cc.SAR::before {
+ content: "\E0CF";
+}
+
+.cc.SCOT-alt::before {
+ content: "\E0D0";
+}
+
+.cc.SCOT::before {
+ content: "\E0D1";
+}
+
+.cc.SDC-alt::before {
+ content: "\E0D2";
+}
+
+.cc.SDC::before {
+ content: "\E0D3";
+}
+
+.cc.SIA-alt::before {
+ content: "\E0D4";
+}
+
+.cc.SIA::before {
+ content: "\E0D5";
+}
+
+.cc.SJCX-alt::before {
+ content: "\E0D6";
+}
+
+.cc.SJCX::before {
+ content: "\E0D7";
+}
+
+.cc.SLG-alt::before {
+ content: "\E0D8";
+}
+
+.cc.SLG::before {
+ content: "\E0D9";
+}
+
+.cc.SLS-alt::before {
+ content: "\E0DA";
+}
+
+.cc.SLS::before {
+ content: "\E0DB";
+}
+
+.cc.SNRG-alt::before {
+ content: "\E0DC";
+}
+
+.cc.SNRG::before {
+ content: "\E0DD";
+}
+
+.cc.START-alt::before {
+ content: "\E0DE";
+}
+
+.cc.START::before {
+ content: "\E0DF";
+}
+
+.cc.STEEM-alt::before {
+ content: "\E0E0";
+}
+
+.cc.STEEM::before {
+ content: "\E0E1";
+}
+
+.cc.STR-alt::before {
+ content: "\E0E2";
+}
+
+.cc.STR::before {
+ content: "\E0E3";
+}
+
+.cc.STRAT-alt::before {
+ content: "\E0E4";
+}
+
+.cc.STRAT::before {
+ content: "\E0E5";
+}
+
+.cc.SWIFT-alt::before {
+ content: "\E0E6";
+}
+
+.cc.SWIFT::before {
+ content: "\E0E7";
+}
+
+.cc.SYNC-alt::before {
+ content: "\E0E8";
+}
+
+.cc.SYNC::before {
+ content: "\E0E9";
+}
+
+.cc.SYS-alt::before {
+ content: "\E0EA";
+}
+
+.cc.SYS::before {
+ content: "\E0EB";
+}
+
+.cc.TRIG-alt::before {
+ content: "\E0EC";
+}
+
+.cc.TRIG::before {
+ content: "\E0ED";
+}
+
+.cc.TX-alt::before {
+ content: "\E0EE";
+}
+
+.cc.TX::before {
+ content: "\E0EF";
+}
+
+.cc.UBQ-alt::before {
+ content: "\E0F0";
+}
+
+.cc.UBQ::before {
+ content: "\E0F1";
+}
+
+.cc.UNITY-alt::before {
+ content: "\E0F2";
+}
+
+.cc.UNITY::before {
+ content: "\E0F3";
+}
+
+.cc.USDT-alt::before {
+ content: "\E0F4";
+}
+
+.cc.USDT::before {
+ content: "\E0F5";
+}
+
+.cc.VIOR-alt::before {
+ content: "\E0F6";
+}
+
+.cc.VIOR::before {
+ content: "\E0F7";
+}
+
+.cc.VNL-alt::before {
+ content: "\E0F8";
+}
+
+.cc.VNL::before {
+ content: "\E0F9";
+}
+
+.cc.VPN-alt::before {
+ content: "\E0FA";
+}
+
+.cc.VPN::before {
+ content: "\E0FB";
+}
+
+.cc.VRC-alt::before {
+ content: "\E0FC";
+}
+
+.cc.VRC::before {
+ content: "\E0FD";
+}
+
+.cc.VTC-alt::before {
+ content: "\E0FE";
+}
+
+.cc.VTC::before {
+ content: "\E0FF";
+}
+
+.cc.WAVES-alt::before {
+ content: "\E100";
+}
+
+.cc.WAVES::before {
+ content: "\E101";
+}
+
+.cc.XAI-alt::before {
+ content: "\E102";
+}
+
+.cc.XAI::before {
+ content: "\E103";
+}
+
+.cc.XBS-alt::before {
+ content: "\E104";
+}
+
+.cc.XBS::before {
+ content: "\E105";
+}
+
+.cc.XCP-alt::before {
+ content: "\E106";
+}
+
+.cc.XCP::before {
+ content: "\E107";
+}
+
+.cc.XEM-alt::before {
+ content: "\E108";
+}
+
+.cc.XEM::before {
+ content: "\E109";
+}
+
+.cc.XMR::before {
+ content: "\E10A";
+}
+
+.cc.XPM-alt::before {
+ content: "\E10B";
+}
+
+.cc.XPM::before {
+ content: "\E10C";
+}
+
+.cc.XRP-alt::before {
+ content: "\E10D";
+}
+
+.cc.XRP::before {
+ content: "\E10E";
+}
+
+.cc.XTZ-alt::before {
+ content: "\E10F";
+}
+
+.cc.XTZ::before {
+ content: "\E110";
+}
+
+.cc.XVG-alt::before {
+ content: "\E111";
+}
+
+.cc.XVG::before {
+ content: "\E112";
+}
+
+.cc.XZC-alt::before {
+ content: "\E113";
+}
+
+.cc.XZC::before {
+ content: "\E114";
+}
+
+.cc.YBC-alt::before {
+ content: "\E115";
+}
+
+.cc.YBC::before {
+ content: "\E116";
+}
+
+.cc.ZEC-alt::before {
+ content: "\E117";
+}
+
+.cc.ZEC::before {
+ content: "\E118";
+}
+
+.cc.ZEIT-alt::before {
+ content: "\E119";
+}
+
+.cc.ZEIT::before {
+ content: "\E11A";
+}
\ No newline at end of file
<title>Cryptoportfolio</title>
<link href='https://fonts.googleapis.com/css?family=Fira+Mono' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
+ <link rel="stylesheet" type="text/css" href="/public/cryptocoins.css"/>
<link rel="stylesheet" type="text/css" href="/public/style.css"/>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
body {
font-family: 'Fira Mono', 'Helvetica Neue', Arial, Helvetica, sans-serif;
background-color: rgb(246, 248, 251);
- text-align: center;
}
ul {
color: inherit;
}
+i.cc {
+ font-size: 1.5em;
+}
+
#logo {
text-align: center;
display: inline-block;
background-color: rgb(250, 250, 250);
box-shadow: 0 2px 6px 2px rgba(0,0,0,.05);
border-radius: 4px;
-}
-
-.box:hover {
- box-shadow: 0 4px 15px 2px rgba(0,0,0,.20);
-}
+ margin-bottom: 10px;
+ margin-top: 10px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
\ No newline at end of file
--- /dev/null
+package markets
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/jloup/poloniex"
+ "github.com/jloup/utils"
+ "github.com/shopspring/decimal"
+)
+
+var (
+ ErrorFlagCounter utils.Counter = 0
+ CurrencyPairNotInTicker = utils.InitFlag(&ErrorFlagCounter, "CurrencyPairNotInTicker")
+ InvalidCredentials = utils.InitFlag(&ErrorFlagCounter, "InvalidCredentials")
+)
+
+func poloniexInvalidCredentialsError(err error) bool {
+ if err == nil {
+ return false
+ }
+ return strings.Contains(err.Error(), "Invalid API key/secret pair")
+}
+
+type CurrencyPair struct {
+ Name string
+ Rate decimal.Decimal
+}
+
+type Poloniex struct {
+ TickerCache map[string]CurrencyPair
+
+ publicClient *poloniex.Poloniex
+ updateTickerChan chan CurrencyPair
+}
+
+func NewPoloniex() *Poloniex {
+ client, _ := poloniex.NewClient("", "")
+
+ return &Poloniex{
+ TickerCache: make(map[string]CurrencyPair),
+ updateTickerChan: nil,
+ publicClient: client,
+ }
+}
+
+func (p *Poloniex) GetBalance(apiKey, apiSecret string) (map[string]decimal.Decimal, error) {
+ client, _ := poloniex.NewClient(apiKey, apiSecret)
+
+ accounts, err := client.TradeReturnAvailableAccountBalances()
+ if poloniexInvalidCredentialsError(err) {
+ return nil, utils.Error{InvalidCredentials, "invalid poloniex credentials"}
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ balances := make(map[string]decimal.Decimal)
+ for currency, balance := range accounts.Margin {
+ balances[currency] = balances[currency].Add(balance)
+ }
+
+ for currency, balance := range accounts.Exchange {
+ balances[currency] = balances[currency].Add(balance)
+ }
+
+ return balances, nil
+}
+
+func (p *Poloniex) ComputeAccountBalanceValue(account map[string]decimal.Decimal, baseCurrency string) (decimal.Decimal, error) {
+ var total decimal.Decimal
+
+ for currency, amount := range account {
+ pair, err := p.GetCurrencyPair(baseCurrency, currency)
+ if err != nil {
+ return decimal.Zero, err
+ }
+
+ total = total.Add(amount.Mul(pair.Rate))
+ }
+
+ return total, nil
+}
+
+func (p *Poloniex) GetCurrencyPair(curr1, curr2 string) (CurrencyPair, error) {
+ pairName := fmt.Sprintf("%s_%s", curr1, curr2)
+ var err error
+
+ if curr1 == curr2 {
+ return CurrencyPair{pairName, decimal.NewFromFloat(1.0)}, nil
+ }
+
+ pair, ok := p.TickerCache[pairName]
+ if !ok {
+ pair, err = p.fetchTicker(curr1, curr2)
+
+ if utils.ErrIs(err, CurrencyPairNotInTicker) {
+ // try to invert an existing ticker.
+ pair, err = p.fetchTicker(curr2, curr1)
+ if err != nil {
+ return CurrencyPair{}, err
+ }
+
+ return CurrencyPair{pairName, decimal.NewFromFloat(1.0).Div(pair.Rate)}, nil
+ }
+
+ if err != nil {
+ return CurrencyPair{}, err
+ }
+ }
+
+ return pair, nil
+}
+
+func (p *Poloniex) fetchTicker(curr1, curr2 string) (CurrencyPair, error) {
+ tickers, err := p.publicClient.PubReturnTickers()
+ if err != nil {
+ return CurrencyPair{}, err
+ }
+
+ pairName := fmt.Sprintf("%s_%s", curr1, curr2)
+
+ if ticker, ok := tickers[pairName]; ok {
+ pair := CurrencyPair{Name: pairName, Rate: ticker.Last}
+
+ if p.updateTickerChan != nil {
+ p.updateTickerChan <- pair
+ }
+
+ return pair, nil
+ }
+
+ return CurrencyPair{}, utils.Error{CurrencyPairNotInTicker, fmt.Sprintf("%s_%s not in ticker", curr1, curr2)}
+}
+
+func (p *Poloniex) StartTicker() error {
+ stream, err := poloniex.NewWSClient()
+ if err != nil {
+ return err
+ }
+
+ err = stream.SubscribeTicker()
+ if err != nil {
+ return err
+ }
+
+ p.updateTickerChan = make(chan CurrencyPair)
+
+ for {
+ quit := false
+ select {
+ case data, ok := <-stream.Subs["ticker"]:
+ if !ok {
+ quit = true
+ } else {
+ ticker := data.(poloniex.WSTicker)
+ if ticker.CurrencyPair == "USDT_BTC" || true {
+ }
+ p.TickerCache[ticker.CurrencyPair] = CurrencyPair{Name: ticker.CurrencyPair, Rate: decimal.NewFromFloat(ticker.Last)}
+ }
+
+ case pair, ok := <-p.updateTickerChan:
+ if !ok {
+ quit = true
+ } else {
+ p.TickerCache[pair.Name] = pair
+ }
+ }
+ if quit {
+ p.updateTickerChan = nil
+ break
+ }
+ }
+
+ return nil
+}