From d323ae391c38998592fc307c41d06b4f9f775a95 Mon Sep 17 00:00:00 2001 From: Philemon Ukane Date: Thu, 15 Aug 2024 02:41:44 +0100 Subject: [PATCH] multi: add support for DCR-USDT pair on `/markets` view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Mexc exchange `DCR-USDT` pair. - Add Binance `DCR-USDT` pair. - Fix minor bugs Breaking Changes: 1. `ratesproto`: Rate messages are no longer `DCR-BTC` only, a new `currencyPair` field indicates which market rate is sent. 
`ExchangeSubscription` field changed from `btcIndex` to `index`. 2. `exchanges`: Aggregated chart data has been removed. This is because combining usdt and btc market bids and asks is not ideal and serves very little purpose. Each market has its own chart. 3. Renamed three fields on `exchanges.ExchangeBotState`: - `btc_index` -> `index` - `dcr_btc_exchanges` -> `dcr_exchanges` (this field now returns a nested map of supported markets) - `btc_indices` -> `indices` (this field now returns a nested map of supported indices) 4. Affected API Endpoints: - `/exchangerate` (`exchanges.ExchangeRates.Exchanges` json field returns a nested map of support markets) - `/exchanges` (returns modified `exchanges.ExchangeBotState`) Signed-off-by: Philemon Ukane --- cmd/dcrdata/go.mod | 22 +- cmd/dcrdata/go.sum | 46 +- cmd/dcrdata/internal/api/apiroutes.go | 14 +- cmd/dcrdata/internal/explorer/explorer.go | 54 +- .../internal/explorer/explorerroutes.go | 6 +- cmd/dcrdata/internal/explorer/websocket.go | 15 +- .../internal/middleware/apimiddleware.go | 16 +- cmd/dcrdata/main.go | 2 +- cmd/dcrdata/public/images/mexc-logo.svg | 13 + .../js/controllers/homepage_controller.js | 12 +- .../js/controllers/market_controller.js | 552 +++-------- cmd/dcrdata/public/scss/icons.scss | 4 + cmd/dcrdata/views/extras.tmpl | 4 +- cmd/dcrdata/views/market.tmpl | 67 +- exchanges/bot.go | 620 ++++++------ exchanges/exchanges.go | 843 +++++++++++------ exchanges/exchanges_live_test.go | 6 - exchanges/exchanges_test.go | 20 +- exchanges/go.mod | 22 +- exchanges/go.sum | 43 +- exchanges/rateserver/config.go | 2 +- exchanges/rateserver/dcrrates-server_test.go | 43 +- exchanges/rateserver/go.mod | 22 +- exchanges/rateserver/go.sum | 44 +- exchanges/rateserver/main.go | 19 +- exchanges/rateserver/types.go | 80 +- exchanges/ratesproto/dcrrates.pb.go | 895 ++++++++++-------- exchanges/ratesproto/dcrrates.proto | 5 +- exchanges/ratesproto/dcrrates_grpc.pb.go | 130 +++ exchanges/ratesproto/runprotoc.sh | 7 +- 30 files changed, 2036 insertions(+), 1592 deletions(-) create mode 100644 cmd/dcrdata/public/images/mexc-logo.svg create mode 100644 exchanges/ratesproto/dcrrates_grpc.pb.go diff --git a/cmd/dcrdata/go.mod b/cmd/dcrdata/go.mod index 0dc165f92..d44ff7c55 100644 --- a/cmd/dcrdata/go.mod +++ b/cmd/dcrdata/go.mod @@ -35,8 +35,8 @@ require ( github.com/jessevdk/go-flags v1.5.0 github.com/jrick/logrotate v1.0.0 github.com/rs/cors v1.8.2 - golang.org/x/net v0.20.0 - golang.org/x/text v0.14.0 + golang.org/x/net v0.26.0 + golang.org/x/text v0.16.0 ) require ( @@ -128,10 +128,10 @@ require ( github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.3.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/trillian v1.4.1 // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/schema v1.1.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect @@ -194,15 +194,15 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zquestz/grab v0.0.0-20190224022517-abcee96e61b1 // indirect go.etcd.io/bbolt v1.3.7-0.20220130032806-d5db64bdbfde // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/grpc v1.61.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect lukechampine.com/blake3 v1.2.1 // indirect diff --git a/cmd/dcrdata/go.sum b/cmd/dcrdata/go.sum index 204e0fc2e..8bd408267 100644 --- a/cmd/dcrdata/go.sum +++ b/cmd/dcrdata/go.sum @@ -766,7 +766,7 @@ github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoB github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -801,8 +801,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -891,8 +891,8 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -1733,8 +1733,8 @@ golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1855,8 +1855,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1894,8 +1894,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180810070207-f0d5e33068cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2016,15 +2016,15 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2035,8 +2035,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2332,8 +2332,8 @@ google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -2377,8 +2377,8 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= @@ -2398,8 +2398,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/cmd/dcrdata/internal/api/apiroutes.go b/cmd/dcrdata/internal/api/apiroutes.go index 5dd518871..4513d82e6 100644 --- a/cmd/dcrdata/internal/api/apiroutes.go +++ b/cmd/dcrdata/internal/api/apiroutes.go @@ -1814,12 +1814,13 @@ func (c *appContext) getCandlestickChart(w http.ResponseWriter, r *http.Request) } token := m.RetrieveExchangeTokenCtx(r) bin := m.RetrieveStickWidthCtx(r) - if token == "" || bin == "" { + currencyPair, error := m.RetrieveCurrencyPair(r) + if token == "" || bin == "" || error != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - chart, err := c.xcBot.QuickSticks(token, bin) + chart, err := c.xcBot.QuickSticks(token, currencyPair, bin) if err != nil { apiLog.Infof("QuickSticks error: %v", err) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) @@ -1835,12 +1836,13 @@ func (c *appContext) getDepthChart(w http.ResponseWriter, r *http.Request) { return } token := m.RetrieveExchangeTokenCtx(r) - if token == "" { + currencyPair, error := m.RetrieveCurrencyPair(r) + if token == "" || error != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - chart, err := c.xcBot.QuickDepth(token) + chart, err := c.xcBot.QuickDepth(token, currencyPair) if err != nil { apiLog.Infof("QuickDepth error: %v", err) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) @@ -1962,7 +1964,7 @@ func (c *appContext) getExchanges(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") var state *exchanges.ExchangeBotState - if code != "" && code != c.xcBot.BtcIndex { + if code != "" && code != c.xcBot.Index { var err error state, err = c.xcBot.ConvertedState(code) if err != nil { @@ -1988,7 +1990,7 @@ func (c *appContext) getExchangeRates(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") var rates *exchanges.ExchangeRates - if code != "" && code != c.xcBot.BtcIndex { + if code != "" && code != c.xcBot.Index { var err error rates, err = c.xcBot.ConvertedRates(code) if err != nil { diff --git a/cmd/dcrdata/internal/explorer/explorer.go b/cmd/dcrdata/internal/explorer/explorer.go index 660338544..5a44ed002 100644 --- a/cmd/dcrdata/internal/explorer/explorer.go +++ b/cmd/dcrdata/internal/explorer/explorer.go @@ -784,21 +784,28 @@ func (exp *explorerUI) watchExchanges() { } xcChans := exp.xcBot.UpdateChannels() - sendXcUpdate := func(isFiat bool, token string, updater *exchanges.ExchangeState) { + sendXcUpdate := func(isFiat bool, token, pair string, updater *exchanges.ExchangeState) { xcState := exp.xcBot.State() update := &WebsocketExchangeUpdate{ Updater: WebsocketMiniExchange{ - Token: token, - Price: updater.Price, - Volume: updater.Volume, - Change: updater.Change, + Token: token, + CurrencyPair: pair, + Price: updater.Price, + Volume: updater.Volume, + Change: updater.Change, }, IsFiatIndex: isFiat, - BtcIndex: exp.xcBot.BtcIndex, + Index: exp.xcBot.Index, Price: xcState.Price, - BtcPrice: xcState.BtcPrice, Volume: xcState.Volume, + Indices: make(map[string]float64), } + + // Other DCR pairs should also provide an index price for the quote + // asset. + update.Indices[exchanges.BTCIndex.String()] = xcState.BtcPrice + update.Indices[exchanges.USDTIndex.String()] = indexPrice(exchanges.USDTIndex, xcState.FiatIndices) + select { case exp.wsHub.xcChan <- update: default: @@ -809,14 +816,22 @@ func (exp *explorerUI) watchExchanges() { for { select { case update := <-xcChans.Exchange: - sendXcUpdate(false, update.Token, update.State) + sendXcUpdate(false, update.Token, update.CurrencyPair.String(), update.State) case update := <-xcChans.Index: - indexState, found := exp.xcBot.State().FiatIndices[update.Token] + currencyIndices, found := exp.xcBot.State().FiatIndices[update.Token] if !found { - log.Errorf("Index state not found when preparing websocket update") + log.Error("Index state not found when preparing websocket update") continue } - sendXcUpdate(true, update.Token, indexState) + + indexState, found := currencyIndices[update.CurrencyPair] + if !found { + log.Errorf("Index state not found for %s when preparing websocket update", update.CurrencyPair) + continue + } + + sendXcUpdate(true, update.Token, update.CurrencyPair.String(), indexState) + case <-xcChans.Quit: log.Warnf("ExchangeBot has quit.") return @@ -844,3 +859,20 @@ func (exp *explorerUI) mempoolTime(txid string) types.TimeDef { } return types.NewTimeDefFromUNIX(tx.Time) } + +// indexPrice is calculates the aggregate index price across all exchanges. +func indexPrice(index exchanges.CurrencyPair, indices map[string]map[exchanges.CurrencyPair]*exchanges.ExchangeState) float64 { + var price, nSources float64 + for _, currecncyIndices := range indices { + for pair, state := range currecncyIndices { + if pair == index { + price += state.Price + nSources++ + } + } + } + if price == 0 { + return 0 + } + return price / nSources +} diff --git a/cmd/dcrdata/internal/explorer/explorerroutes.go b/cmd/dcrdata/internal/explorer/explorerroutes.go index 323601806..8a9b15278 100644 --- a/cmd/dcrdata/internal/explorer/explorerroutes.go +++ b/cmd/dcrdata/internal/explorer/explorerroutes.go @@ -2424,7 +2424,7 @@ func (exp *explorerUI) HandleApiRequestsOnSync(w http.ResponseWriter, r *http.Re dataFetched := SyncStatus() syncStatus := "in progress" - if len(dataFetched) == complete { + if len(dataFetched) == complete && !exp.ShowingSyncStatusPage() { syncStatus = "complete" } @@ -2462,9 +2462,7 @@ func (exp *explorerUI) HandleApiRequestsOnSync(w http.ResponseWriter, r *http.Re func (exp *explorerUI) MarketPage(w http.ResponseWriter, r *http.Request) { str, err := exp.templates.exec("market", struct { *CommonPageData - DepthMarkets []string - StickMarkets map[string]string - XcState *exchanges.ExchangeBotState + XcState *exchanges.ExchangeBotState }{ CommonPageData: exp.commonData(r), XcState: exp.getExchangeState(), diff --git a/cmd/dcrdata/internal/explorer/websocket.go b/cmd/dcrdata/internal/explorer/websocket.go index 20b382508..517f662bf 100644 --- a/cmd/dcrdata/internal/explorer/websocket.go +++ b/cmd/dcrdata/internal/explorer/websocket.go @@ -374,10 +374,11 @@ const exchangeUpdateID = "exchange" // WebsocketMiniExchange is minimal info regarding the exchange that triggered // an update. type WebsocketMiniExchange struct { - Token string `json:"token"` - Price float64 `json:"price"` - Volume float64 `json:"volume"` - Change float64 `json:"change"` + Token string `json:"token"` + CurrencyPair string `json:"pair"` + Price float64 `json:"price"` + Volume float64 `json:"volume"` + Change float64 `json:"change"` } // WebsocketExchangeUpdate is an update to the exchange state to send over the @@ -385,8 +386,10 @@ type WebsocketMiniExchange struct { type WebsocketExchangeUpdate struct { Updater WebsocketMiniExchange `json:"updater"` IsFiatIndex bool `json:"fiat"` - BtcIndex string `json:"index"` + Index string `json:"index"` Price float64 `json:"price"` - BtcPrice float64 `json:"btc_price"` Volume float64 `json:"volume"` + // Indices is a map of supported indices to their index price, e.g + // BTC-Index, USDT-Index. + Indices map[string]float64 `json:"indices"` } diff --git a/cmd/dcrdata/internal/middleware/apimiddleware.go b/cmd/dcrdata/internal/middleware/apimiddleware.go index 49fbedb18..71386744e 100644 --- a/cmd/dcrdata/internal/middleware/apimiddleware.go +++ b/cmd/dcrdata/internal/middleware/apimiddleware.go @@ -22,6 +22,7 @@ import ( chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4" "github.com/decred/dcrd/txscript/v4/stdaddr" "github.com/decred/dcrd/wire" + "github.com/decred/dcrdata/exchanges/v3" apitypes "github.com/decred/dcrdata/v8/api/types" "github.com/didip/tollbooth/v6" "github.com/didip/tollbooth/v6/limiter" @@ -502,7 +503,7 @@ func GetOffsetCtx(r *http.Request) int { // GetPageNumCtx retrieves the ctxPageNum data ("pageNum") URL path element from // the request context. If not set, the return value is 1. The page number must -// be a postitive integer. +// be a positive integer. func GetPageNumCtx(r *http.Request) int { pageNum, ok := r.Context().Value(ctxPageNum).(int) if !ok { @@ -1101,3 +1102,16 @@ func RetrieveStickWidthCtx(r *http.Request) string { } return bin } + +// RetrieveCurrencyPair tries to fetch the currency pair from the request query. +func RetrieveCurrencyPair(r *http.Request) (exchanges.CurrencyPair, error) { + pair := exchanges.CurrencyPair(r.URL.Query().Get("currencyPair")) + if pair == "" { + // Use the DCR-BTC pair for backward compatibility. + pair = exchanges.CurrencyPairDCRBTC + } + if !pair.IsValidDCRPair() { + return "", fmt.Errorf("invalid currency pair (%s)", pair) + } + return pair, nil +} diff --git a/cmd/dcrdata/main.go b/cmd/dcrdata/main.go index ca925862d..5ed075f0a 100644 --- a/cmd/dcrdata/main.go +++ b/cmd/dcrdata/main.go @@ -417,7 +417,7 @@ func _main(ctx context.Context) error { } if cfg.EnableExchangeBot { botCfg := exchanges.ExchangeBotConfig{ - BtcIndex: cfg.ExchangeCurrency, + Index: cfg.ExchangeCurrency, MasterBot: cfg.RateMaster, MasterCertFile: cfg.RateCertificate, } diff --git a/cmd/dcrdata/public/images/mexc-logo.svg b/cmd/dcrdata/public/images/mexc-logo.svg new file mode 100644 index 000000000..52acb371f --- /dev/null +++ b/cmd/dcrdata/public/images/mexc-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/cmd/dcrdata/public/js/controllers/homepage_controller.js b/cmd/dcrdata/public/js/controllers/homepage_controller.js index bced50b7b..02f117801 100644 --- a/cmd/dcrdata/public/js/controllers/homepage_controller.js +++ b/cmd/dcrdata/public/js/controllers/homepage_controller.js @@ -200,24 +200,24 @@ export default class extends Controller { if (ex.exchange_rate) { const xcRate = ex.exchange_rate.value - const btcIndex = ex.exchange_rate.index + const index = ex.exchange_rate.index if (this.hasPowConvertedTarget) { - this.powConvertedTarget.textContent = `${humanize.twoDecimals(ex.subsidy.pow / 1e8 * xcRate)} ${btcIndex}` + this.powConvertedTarget.textContent = `${humanize.twoDecimals(ex.subsidy.pow / 1e8 * xcRate)} ${index}` } if (this.hasConvertedDevTarget) { - this.convertedDevTarget.textContent = `${humanize.threeSigFigs(treasuryTotal / 1e8 * xcRate)} ${btcIndex}` + this.convertedDevTarget.textContent = `${humanize.threeSigFigs(treasuryTotal / 1e8 * xcRate)} ${index}` } if (this.hasConvertedSupplyTarget) { - this.convertedSupplyTarget.textContent = `${humanize.threeSigFigs(ex.coin_supply / 1e8 * xcRate)} ${btcIndex}` + this.convertedSupplyTarget.textContent = `${humanize.threeSigFigs(ex.coin_supply / 1e8 * xcRate)} ${index}` } if (this.hasConvertedDevSubTarget) { - this.convertedDevSubTarget.textContent = `${humanize.twoDecimals(ex.subsidy.dev / 1e8 * xcRate)} ${btcIndex}` + this.convertedDevSubTarget.textContent = `${humanize.twoDecimals(ex.subsidy.dev / 1e8 * xcRate)} ${index}` } if (this.hasExchangeRateTarget) { this.exchangeRateTarget.textContent = humanize.twoDecimals(xcRate) } if (this.hasConvertedStakeTarget) { - this.convertedStakeTarget.textContent = `${humanize.twoDecimals(ex.sdiff * xcRate)} ${btcIndex}` + this.convertedStakeTarget.textContent = `${humanize.twoDecimals(ex.sdiff * xcRate)} ${index}` } } } diff --git a/cmd/dcrdata/public/js/controllers/market_controller.js b/cmd/dcrdata/public/js/controllers/market_controller.js index 81f44f8b0..08b1da072 100644 --- a/cmd/dcrdata/public/js/controllers/market_controller.js +++ b/cmd/dcrdata/public/js/controllers/market_controller.js @@ -1,10 +1,10 @@ import { Controller } from '@hotwired/stimulus' -import TurboQuery from '../helpers/turbolinks_helper' -import { getDefault } from '../helpers/module_helper' import { requestJSON } from '../helpers/http' import humanize from '../helpers/humanize_helper' -import { darkEnabled } from '../services/theme_service' +import { getDefault } from '../helpers/module_helper' +import TurboQuery from '../helpers/turbolinks_helper' import globalEventBus from '../services/event_bus_service' +import { darkEnabled } from '../services/theme_service' let Dygraph const SELL = 1 @@ -32,12 +32,36 @@ const prettyDurations = { '1mo': 'month' } const exchangeLinks = { - binance: 'https://www.binance.com/en/trade/DCR_BTC', - bittrex: 'https://bittrex.com/Market/Index?MarketName=BTC-DCR', - poloniex: 'https://poloniex.com/exchange#btc_dcr', - dragonex: 'https://dragonex.io/en-us/trade/index/dcr_btc', - huobi: 'https://www.hbg.com/en-us/exchange/?s=dcr_btc', - dcrdex: 'https://dex.decred.org' + CurrencyPairDCRBTC: { + binance: 'https://www.binance.com/en/trade/DCR_BTC', + bittrex: 'https://bittrex.com/Market/Index?MarketName=BTC-DCR', + poloniex: 'https://poloniex.com/exchange#btc_dcr', + dragonex: 'https://dragonex.io/en-us/trade/index/dcr_btc', + huobi: 'https://www.hbg.com/en-us/exchange/?s=dcr_btc', + dcrdex: 'https://dex.decred.org' + }, + CurrencyPairDCRUSDT: { + binance: 'https://www.binance.com/en/trade/DCR_USDT', + dcrdex: 'https://dex.decred.org', + mexc: 'https://www.mexc.com/exchange/DCR_USDT' + } +} +const CurrencyPairDCRUSDT = 'DCR-USDT' +const CurrencyPairDCRBTC = 'DCR-BTC' + +function isValidDCRPair (pair) { + return pair === CurrencyPairDCRBTC || pair === CurrencyPairDCRUSDT +} + +const BTCIndex = 'BTC-Index' +const USDTIndex = 'USDT-Index' + +function quoteAsset (currencyPair) { + const v = currencyPair.split('-') + if (v.length === 1) { + return currencyPair + } + return v[1].toUpperCase() } const printNames = { @@ -64,7 +88,6 @@ if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and visibilityChange = 'webkitvisibilitychange' } let focused = true -let aggStacking = true let refreshAvailable = false let availableCandlesticks, availableDepths @@ -102,54 +125,26 @@ function clearCache (k) { delete responseCache[k] } +let indices = {} +function currentPairFiatPrice () { + switch (settings.pair) { + case CurrencyPairDCRBTC: + return indices[BTCIndex] + case CurrencyPairDCRUSDT: + return indices[USDTIndex] + default: + return -1 + } +} + const lightStroke = '#333' const darkStroke = '#ddd' let chartStroke = lightStroke let conversionFactor = 1 -let btcPrice, fiatCode +let fiatCode const gridColor = '#7774' let settings = {} -let colorNumerator = 0 -let colorDenominator = 1 -const hslS = '100%' -const hslL = '50%' -const hslOffset = 225 // 0 <= x < 360 - -// These are the first four hues generated by getHue( -const exchangeHues = { - dcrdex: 'hsl(225,100%,50%)', - binance: 'hsl(45,100%,50%)', - bittrex: 'hsl(315,100%,50%)', - poloniex: 'hsl(135,100%,50%)' -} - -const hsl = (h) => `hsl(${(h + hslOffset) % 360},${hslS},${hslL})` -// Generates colors on the hue sequence 0, 1/2, 1/4, 3/4, 1/8, 3/8, 5/8, 7/8, 1/16, ... -function generateHue () { - if (colorNumerator >= colorDenominator) { - colorNumerator = 1 // reset the numerator - colorDenominator *= 2 // double the denominator - if (colorDenominator >= 512) { // Will generate 256 different hues - colorNumerator = 0 - colorDenominator = 1 - } - return generateHue() - } - const hue = colorNumerator / colorDenominator * 360 - colorNumerator += 2 - return hsl(hue) -} - -function getHue (token) { - if (exchangeHues[token]) return exchangeHues[token] - exchangeHues[token] = generateHue() - return exchangeHues[token] -} - -// Generate the constant hues so dynamically assigned hues won't use them. -Object.keys(exchangeHues).forEach(generateHue) - const commonChartOpts = { gridLineColor: gridColor, axisLineColor: 'transparent', @@ -172,7 +167,6 @@ const chartResetOpts = { logscale: false, xRangePad: 0, yRangePad: 0, - stackedGraph: false, zoomCallback: null } @@ -235,14 +229,6 @@ const dummyOrderbook = { } } -function sizedArray (len, v) { - const a = [] - for (let i = 0; i < len; i++) { - a.push(v) - } - return a -} - function rangedPts (pts, cutoff) { const l = [] const outliers = [] @@ -268,41 +254,6 @@ function translateDepthSide (pts, idx, cutoff) { return { pts: translated, outliers: sorted.outliers } } -function translateDepthPoint (pt, offset, accumulator) { - const l = sizedArray(pt.volumes.length + 1, null) - l[0] = pt.price - pt.volumes.forEach((vol, i) => { - accumulator[i] += vol - l[offset + i + 1] = accumulator[i] - }) - return l -} - -function needsDummyPoint (pt, offset, accumulator) { - const xcCount = pt.volumes.length - for (let i = 0; i < xcCount; i++) { - if (pt.volumes[i] && accumulator[i] === 0) return { price: pt.price + offset, volumes: sizedArray(xcCount, 0) } - } - return false -} - -function translateAggregatedDepthSide (pts, idx, cutoff) { - const sorted = rangedPts(pts, cutoff) - const xcCount = pts[0].volumes.length - const offset = idx === SELL ? 0 : xcCount - const zeroWidth = idx === SELL ? -1e-8 : 1e-8 - const xcAccumulator = sizedArray(xcCount, 0) - const l = [] - sorted.pts.forEach(pt => { - const zeros = needsDummyPoint(pt, zeroWidth, xcAccumulator) - if (zeros) { - l.push(translateDepthPoint(zeros, offset, xcAccumulator)) - } - l.push(translateDepthPoint(pt, offset, xcAccumulator)) - }) - return { pts: l, outliers: sorted.outliers } -} - function translateOrderbookSide (pts, idx, cutoff) { const sorted = rangedPts(pts, cutoff) const translated = sorted.pts.map(pt => { @@ -313,30 +264,9 @@ function translateOrderbookSide (pts, idx, cutoff) { return { pts: translated, outliers: sorted.outliers } } -function sumPt (pt) { - return pt.volumes.reduce((a, v) => { return a + v }, 0) -} - -function translateAggregatedOrderbookSide (pts, idx, cutoff) { - const sorted = rangedPts(pts, cutoff) - const translated = sorted.pts.map(pt => { - const l = [pt.price, null, null] - l[idx] = sumPt(pt) - return l - }) - return { pts: translated, outliers: sorted.outliers } -} - function processOrderbook (response, translator) { const bids = response.data.bids const asks = response.data.asks - - if (!response.tokens) { - // Add the dummy points to make the chart line connect to the baseline and - // because otherwise Dygraph has a bug that adds an offset to the asks side. - bids.splice(0, 0, { price: bids[0].price + 1e-8, quantity: 0 }) - asks.splice(0, 0, { price: asks[0].price - 1e-8, quantity: 0 }) - } if (!bids || !asks) { console.warn('no bid/ask data in API response') return dummyOrderbook @@ -345,27 +275,24 @@ function processOrderbook (response, translator) { console.warn('empty bid/ask data in API response') return dummyOrderbook } + // Add the dummy points to make the chart line connect to the baseline and + // because otherwise Dygraph has a bug that adds an offset to the asks side. + bids.splice(0, 0, { price: bids[0].price + 1e-8, quantity: 0 }) + asks.splice(0, 0, { price: asks[0].price - 1e-8, quantity: 0 }) const stats = orderbookStats(bids, asks) const buys = translator(bids, BUY, pt => pt.price < stats.lowCut) buys.pts.reverse() const sells = translator(asks, SELL, pt => pt.price > stats.highCut) - // Find points in overlapping region with duplicate rates, to deal with a - // Dygraphs bug. - let dupes - if (response.tokens) dupes = findAggregateDupes(buys.pts, sells.pts) - return { pts: buys.pts.concat(sells.pts), outliers: buys.outliers.concat(sells.outliers), - stats: stats, - dupes: dupes + stats: stats } } function candlestickPlotter (e) { if (e.seriesIndex !== 0) return - const area = e.plotArea const ctx = e.drawingContext ctx.strokeStyle = chartStroke @@ -467,7 +394,6 @@ function orderPlotter (e) { const greekCapDelta = String.fromCharCode(916) function depthLegendPlotter (e) { - const tokens = e.dygraph.getOption('tokens') const stats = e.dygraph.getOption('stats') const area = e.plotArea @@ -487,8 +413,8 @@ function depthLegendPlotter (e) { const midGapPrice = humanize.threeSigFigs(stats.midGap) const deltaPctTxt = `${greekCapDelta} : ${humanize.threeSigFigs(stats.gap / stats.midGap * 100)}%` - const fiatGapTxt = `${humanize.threeSigFigs(stats.gap * btcPrice)} ${fiatCode}` - const btcGapTxt = `${humanize.threeSigFigs(stats.gap)} BTC` + const fiatGapTxt = `${humanize.threeSigFigs(stats.gap * currentPairFiatPrice())} ${fiatCode}` + const btcGapTxt = `${humanize.threeSigFigs(stats.gap)} ${quoteAsset(settings.pair)}` let boxW = 0 const txts = [fiatGapTxt, btcGapTxt, deltaPctTxt, midGapPrice] txts.forEach(txt => { @@ -498,38 +424,9 @@ function depthLegendPlotter (e) { let rowHeight = fontSize * 1.5 const rowPad = big ? (rowHeight - fontSize) / 2 : (rowHeight - fontSize) / 3 const boxPad = big ? rowHeight / 3 : rowHeight / 5 - let x let y = big ? fontSize * 2 : fontSize - - // If it's an aggregated chart, start with a color legend - if (tokens) { - // If this is an aggregated chart, draw the color legend first - const ptSize = fontSize / 3 - let legW = 0 - tokens.forEach(token => { - const w = ctx.measureText(token).width + rowHeight// leave space for dot - if (w > legW) legW = w - }) - x = midGap.x - legW / 2 - const boxH = rowHeight * tokens.length - ctx.fillStyle = boxColor - const rect = makePt(x - boxPad, y - boxPad) - const dims = makePt(legW + boxPad * 4, boxH + boxPad * 2) - ctx.fillRect(rect.x, rect.y, dims.x, dims.y) - ctx.strokeRect(rect.x, rect.y, dims.x, dims.y) - tokens.forEach(token => { - ctx.fillStyle = getHue(token) - drawPt(ctx, makePt(x + rowHeight / 2, y + rowHeight / 2 - 1), ptSize) - ctx.fillStyle = chartStroke - ctx.fillText(token, x + rowPad + rowHeight, y + rowPad) - y += rowHeight - }) - y += boxPad * 3 - x = midGap.x - boxW / 2 - } else { - y += area.h / 4 - x = midGap.x - boxW / 2 - 25 - } + y += area.h / 4 + const x = midGap.x - boxW / 2 - 25 // Label the gap size. rowHeight -= 2 // just looks better ctx.fillStyle = boxColor @@ -561,23 +458,13 @@ function depthLegendPlotter (e) { function depthPlotter (e) { Dygraph.Plotters.fillPlotter(e) - const tokens = e.dygraph.getOption('tokens') - if (tokens && e.dygraph.getOption('stackedGraph')) { - if (e.seriesIndex === 0 || e.seriesIndex === tokens.length) { - e.color = chartStroke - } else { - e.color = 'transparent' - } - fixAggregateStacking(e) - } - Dygraph.Plotters.linePlotter(e) // Callout box with color legend if (e.seriesIndex === e.allSeriesPoints.length - 1) depthLegendPlotter(e) } -let stickZoom, orderZoom +let stickZoom function calcStickWindow (start, end, bin) { const halfBin = minuteMap[bin] / 2 start = new Date(start.getTime()) @@ -588,17 +475,21 @@ function calcStickWindow (start, end, bin) { ] } +function isValidExchange (xc) { + return xc === 'binance' || xc === 'dcrdex' || xc === 'poloniex' || + xc === 'bittrex' || xc === 'huobi' || xc === 'dragonex' || xc === 'mexc' +} + export default class extends Controller { static get targets () { return ['chartSelect', 'exchanges', 'bin', 'chart', 'legend', 'conversion', 'xcName', 'xcLogo', 'actions', 'sticksOnly', 'depthOnly', 'chartLoader', - 'xcRow', 'xcIndex', 'price', 'age', 'ageSpan', 'link', 'aggOption', - 'aggStack', 'zoom'] + 'xcRow', 'xcIndex', 'price', 'age', 'ageSpan', 'link', 'zoom', 'marketName', 'marketSection'] } async connect () { this.query = new TurboQuery() - settings = TurboQuery.nullTemplate(['chart', 'xc', 'bin']) + settings = TurboQuery.nullTemplate(['chart', 'xc', 'bin', 'pair']) this.query.update(settings) this.processors = { orders: this.processOrders, @@ -609,7 +500,7 @@ export default class extends Controller { } commonChartOpts.labelsDiv = this.legendTarget this.converted = false - btcPrice = parseFloat(this.conversionTarget.dataset.factor) + indices = JSON.parse(this.conversionTarget.dataset.indices) fiatCode = this.conversionTarget.dataset.code this.binButtons = this.binTarget.querySelectorAll('button') this.lastUrl = null @@ -618,11 +509,9 @@ export default class extends Controller { availableCandlesticks = {} availableDepths = [] - this.exchangeOptions = [] let opts = this.exchangesTarget.options for (let i = 0; i < opts.length; i++) { const option = opts[i] - this.exchangeOptions.push(option) if (option.dataset.sticks) { availableCandlesticks[option.value] = option.dataset.bins.split(';') } @@ -638,12 +527,11 @@ export default class extends Controller { if (settings.chart == null) { settings.chart = depth } - if (settings.xc == null) { - settings.xc = usesOrderbook(settings.chart) ? aggregatedKey : 'binance' + if (!isValidExchange(settings.xc)) { + settings.xc = 'binance' } - if (settings.stack) { - settings.stack = parseInt(settings.stack) - if (settings.stack === 0) aggStacking = false + if (!isValidDCRPair(settings.pair)) { + settings.pair = CurrencyPairDCRUSDT } this.setExchangeName() if (settings.bin == null) { @@ -741,24 +629,25 @@ export default class extends Controller { const thisRequest = requestCounter const bin = settings.bin const xc = settings.xc + const cacheKey = this.xcTokenAndPair() const chart = settings.chart const oldZoom = this.graph.xAxisRange() if (usesCandlesticks(chart)) { - if (!(xc in availableCandlesticks)) { - console.warn('invalid candlestick exchange:', xc) + if (!(cacheKey in availableCandlesticks)) { + console.warn('invalid candlestick exchange:', cacheKey) return } - if (availableCandlesticks[xc].indexOf(bin) === -1) { + if (availableCandlesticks[cacheKey].indexOf(bin) === -1) { console.warn('invalid bin:', bin) return } - url = `/api/chart/market/${xc}/candlestick/${bin}` + url = `/api/chart/market/${xc}/candlestick/${bin}?currencyPair=${settings.pair}` } else if (usesOrderbook(chart)) { - if (!validDepthExchange(xc)) { - console.warn('invalid depth exchange:', xc) + if (!validDepthExchange(cacheKey)) { + console.warn('invalid depth exchange:', cacheKey) return } - url = `/api/chart/market/${xc}/depth` + url = `/api/chart/market/${xc}/depth?currencyPair=${settings.pair}` } if (!url) { console.warn('invalid chart:', chart) @@ -800,6 +689,10 @@ export default class extends Controller { refreshAvailable = false } + xcTokenAndPair () { + return settings.xc + ':' + settings.pair + } + processCandlesticks (response) { const halfDuration = minuteMap[settings.bin] / 2 const data = response.sticks.map(stick => { @@ -818,7 +711,7 @@ export default class extends Controller { file: data, labels: ['time', 'open', 'close', 'high', 'low'], xlabel: 'Time', - ylabel: 'Price (BTC)', + ylabel: `Price (${quoteAsset(settings.pair)})`, plotter: candlestickPlotter, axes: { x: { @@ -845,7 +738,7 @@ export default class extends Controller { }), labels: ['time', 'price'], xlabel: 'Time', - ylabel: 'Price (BTC)', + ylabel: `Price (${quoteAsset(settings.pair)})`, colors: [chartStroke], plotter: Dygraph.Plotters.linePlotter, axes: { @@ -888,18 +781,14 @@ export default class extends Controller { } processDepth (response) { - if (response.tokens) { - return this.processAggregateDepth(response) - } const data = processOrderbook(response, translateDepthSide) return { labels: ['price', 'cumulative sell', 'cumulative buy'], file: data.pts, fillGraph: true, colors: ['#ed6d47', '#41be53'], - xlabel: `Price (${this.converted ? fiatCode : 'BTC'})`, + xlabel: `Price (${this.converted ? fiatCode : quoteAsset(settings.pair)})`, ylabel: 'Volume (DCR)', - tokens: null, stats: data.stats, plotter: depthPlotter, // Don't use Dygraph.linePlotter here. fillGraph won't work. zoomCallback: this.zoomCallback, @@ -916,58 +805,16 @@ export default class extends Controller { } } - processAggregateDepth (response) { - // Re-order the data so that deepest books are first. - reorderAggregateData(response) - const tokens = response.tokens - const data = processOrderbook(response, translateAggregatedDepthSide) - const xcCount = tokens.length - const keys = sizedArray(xcCount * 2 + 1, null) - keys[0] = 'price' - const colors = sizedArray(xcCount * 2, null) - for (let i = 0; i < xcCount; i++) { - const token = tokens[i] - const color = getHue(token) - keys[i + 1] = ` ${token} sell` - keys[xcCount + i + 1] = ` ${token} buy` - colors[i] = color - colors[xcCount + i] = color - } - return { - labels: keys, - file: data.pts, - colors: colors, - xlabel: `Price (${this.converted ? fiatCode : 'BTC'})`, - ylabel: 'Volume (DCR)', - plotter: depthPlotter, - fillGraph: aggStacking, - stackedGraph: aggStacking, - tokens: tokens, - stats: data.stats, - dupes: data.dupes, - zoomCallback: this.zoomCallback, - axes: { - x: { - axisLabelFormatter: convertedThreeSigFigs, - valueFormatter: convertedEightDecimals - }, - y: { - axisLabelFormatter: humanize.threeSigFigs, - valueFormatter: humanize.threeSigFigs - } - } - } - } - processOrders (response) { - const data = processOrderbook(response, response.tokens ? translateAggregatedOrderbookSide : translateOrderbookSide) + const data = processOrderbook(response, translateOrderbookSide) return { labels: ['price', 'sell', 'buy'], file: data.pts, colors: ['#f93f39cc', '#1acc84cc'], - xlabel: `Price (${this.converted ? fiatCode : 'BTC'})`, + xlabel: `Price (${this.converted ? fiatCode : quoteAsset(settings.pair)})`, ylabel: 'Volume (DCR)', plotter: orderPlotter, + stats: data.stats, axes: { x: { axisLabelFormatter: convertedThreeSigFigs @@ -986,7 +833,7 @@ export default class extends Controller { } justifyBins () { - const bins = availableCandlesticks[settings.xc] + const bins = availableCandlesticks[this.xcTokenAndPair()] if (bins.indexOf(settings.bin) === -1) { settings.bin = bins[0] this.setBinSelection() @@ -995,17 +842,15 @@ export default class extends Controller { setButtons () { this.chartSelectTarget.value = settings.chart - this.exchangesTarget.value = settings.xc + this.exchangesTarget.value = this.xcTokenAndPair() if (usesOrderbook(settings.chart)) { this.binTarget.classList.add('d-hide') - this.aggOptionTarget.disabled = false this.zoomTarget.classList.remove('d-hide') } else { this.binTarget.classList.remove('d-hide') - this.aggOptionTarget.disabled = true this.zoomTarget.classList.add('d-hide') this.binButtons.forEach(button => { - if (hasBin(settings.xc, button.name)) { + if (hasBin(this.exchangesTarget.value, button.name)) { button.classList.remove('d-hide') } else { button.classList.add('d-hide') @@ -1013,21 +858,14 @@ export default class extends Controller { }) this.setBinSelection() } - const sticksDisabled = !availableCandlesticks[settings.xc] + const sticksDisabled = !availableCandlesticks[this.exchangesTarget.value] this.sticksOnlyTargets.forEach(option => { option.disabled = sticksDisabled }) - const depthDisabled = !validDepthExchange(settings.xc) + const depthDisabled = !validDepthExchange(this.exchangesTarget.value) this.depthOnlyTargets.forEach(option => { option.disabled = depthDisabled }) - if (settings.xc === aggregatedKey && settings.chart === depth) { - this.aggStackTarget.classList.remove('d-hide') - settings.stack = aggStacking ? 1 : 0 - } else { - this.aggStackTarget.classList.add('d-hide') - settings.stack = null - } } setBinSelection () { @@ -1052,10 +890,11 @@ export default class extends Controller { } changeExchange () { - settings.xc = this.exchangesTarget.value + settings.xc = this.exchangesTarget.value.split(':')[0] + settings.pair = this.exchangesTarget.value.split(':')[1] this.setExchangeName() if (usesCandlesticks(settings.chart)) { - if (!availableCandlesticks[settings.xc]) { + if (!availableCandlesticks[this.exchangesTarget.value]) { // exchange does not have candlestick data // show the depth chart. settings.chart = depth @@ -1072,7 +911,9 @@ export default class extends Controller { let node = e.target || e.srcElement while (node && node.nodeName !== 'TR') node = node.parentNode if (!node || !node.dataset || !node.dataset.token) return - this.exchangesTarget.value = node.dataset.token + settings.xc = node.dataset.token + settings.pair = node.dataset.pair + this.exchangesTarget.value = this.xcTokenAndPair() this.changeExchange() } @@ -1089,8 +930,7 @@ export default class extends Controller { if (settings.chart === candlestick) { this.graph.updateOptions({ dateWindow: stickZoom }) } else if (usesOrderbook(settings.chart)) { - if (orderZoom) this.graph.updateOptions({ dateWindow: orderZoom }) - else this.setZoomPct(defaultZoomPct) + this.setZoomPct(defaultZoomPct) } else { this.graph.resetZoom() } @@ -1109,23 +949,30 @@ export default class extends Controller { if (btn.nodeName !== 'BUTTON' || !this.graph) return this.conversionTarget.querySelectorAll('button').forEach(b => b.classList.remove('btn-selected')) btn.classList.add('btn-selected') - let cLabel = 'BTC' - if (e.target.name === 'BTC') { + this.updateConversion(e.target.name) + } + + updateConversion (targetName) { + if (!this.graph) return + let cLabel = quoteAsset(settings.pair) + if (targetName === cLabel) { this.converted = false conversionFactor = 1 } else { this.converted = true - conversionFactor = btcPrice + conversionFactor = currentPairFiatPrice() cLabel = fiatCode } this.graph.updateOptions({ xlabel: `Price (${cLabel})` }) } setExchangeName () { - this.xcLogoTarget.className = `exchange-logo ${settings.xc}` + this.xcLogoTarget.className = `exchange-logo ${settings.xc} me-2` const prettyName = printName(settings.xc) this.xcNameTarget.textContent = prettyName - const href = exchangeLinks[settings.xc] + let href + if (settings.pair === CurrencyPairDCRUSDT) href = exchangeLinks.CurrencyPairDCRUSDT[settings.xc] + else href = exchangeLinks.CurrencyPairDCRBTC[settings.xc] if (href) { this.linkTarget.href = href this.linkTarget.textContent = `Visit ${prettyName}` @@ -1133,6 +980,16 @@ export default class extends Controller { } else { this.actionsTarget.classList.add('d-hide') } + this.conversionTarget.querySelectorAll('button').forEach(b => { + if (b.textContent !== fiatCode) { + b.name = quoteAsset(settings.pair) + b.textContent = b.name + b.classList.add('btn-selected') + this.updateConversion(b.textContent) + } else { + b.classList.remove('btn-selected') + } + }) } _processNightMode (data) { @@ -1146,11 +1003,12 @@ export default class extends Controller { } } - getExchangeRow (token) { + getExchangeRow (token, pair) { const rows = this.xcRowTargets for (let i = 0; i < rows.length; i++) { const tr = rows[i] - if (tr.dataset.token === token) { + const hasPair = tr.dataset.pair !== undefined && tr.dataset.pair !== null && tr.dataset.pair === pair + if ((hasPair && tr.dataset.token === token) || tr.dataset.token === token) { const row = {} tr.querySelectorAll('td').forEach(td => { switch (td.dataset.type) { @@ -1174,15 +1032,6 @@ export default class extends Controller { return null } - setStacking (e) { - const btn = e.target || e.srcElement - if (btn.nodeName !== 'BUTTON' || !this.graph) return - this.aggStackTarget.querySelectorAll('button').forEach(b => b.classList.remove('btn-selected')) - btn.classList.add('btn-selected') - aggStacking = btn.name === 'on' - this.graph.updateOptions({ stackedGraph: aggStacking, fillGraph: aggStacking }) - } - setZoom (e) { const btn = e.target || e.srcElement if (btn.nodeName !== 'BUTTON' || !this.graph) return @@ -1204,28 +1053,33 @@ export default class extends Controller { const [min, max] = this.graph.xAxisExtremes() if (low < min) low = min if (high > max) high = max - orderZoom = [low, high] - this.graph.updateOptions({ dateWindow: orderZoom }) + this.graph.updateOptions({ dateWindow: [low, high] }) } - _zoomCallback (start, end) { - orderZoom = [start, end] + _zoomCallback () { this.zoomButtons.forEach(b => b.classList.remove('btn-selected')) } _processXcUpdate (update) { const xc = update.updater + indices = update.indices if (update.fiat) { // btc-fiat exchange update - this.xcIndexTargets.forEach(span => { - if (span.dataset.token === xc.token) { - span.textContent = humanize.commaWithDecimal(xc.price, 2) - } - }) - } else { // dcr-btc exchange update - const row = this.getExchangeRow(xc.token) + if (xc.pair === BTCIndex) { // we also receive updates for USDTIndex but we don't use it atm. + this.xcIndexTargets.forEach(span => { + if (span.dataset.token === xc.token) { + span.textContent = humanize.commaWithDecimal(xc.price, 2) + } + }) + } + } else { // dcr-{Asset} exchange update + const row = this.getExchangeRow(xc.token, xc.pair) row.volume.textContent = humanize.threeSigFigs(xc.volume) row.price.textContent = humanize.threeSigFigs(xc.price) - row.fiat.textContent = (xc.price * update.btc_price).toFixed(2) + if (xc.pair === CurrencyPairDCRBTC) { + row.fiat.textContent = (xc.price * indices[BTCIndex]).toFixed(2) + } else if (xc.pair === CurrencyPairDCRUSDT) { + row.fiat.textContent = (xc.price * indices[USDTIndex]).toFixed(2) + } if (xc.change === 0) { row.arrow.className = '' } else if (xc.change > 0) { @@ -1238,15 +1092,10 @@ export default class extends Controller { const fmtPrice = update.price.toFixed(2) this.priceTarget.textContent = fmtPrice const aggRow = this.getExchangeRow(aggregatedKey) - btcPrice = update.btc_price + const btcPrice = indices[BTCIndex] aggRow.price.textContent = humanize.threeSigFigs(update.price / btcPrice) aggRow.volume.textContent = humanize.threeSigFigs(update.volume) aggRow.fiat.textContent = fmtPrice - // Auto-update the chart if it makes sense. - if (settings.xc !== aggregatedKey && settings.xc !== xc.token) return - if (settings.xc === aggregatedKey && - hasCache(this.lastUrl) && - responseCache[this.lastUrl].tokens.indexOf(update.updater) === -1) return if (usesOrderbook(settings.chart)) { clearCache(this.lastUrl) this.refreshChart() @@ -1258,122 +1107,3 @@ export default class extends Controller { } } } - -function aggregateSums (side, sums, tokens, cutoff) { - for (const pt of side) { - if (cutoff(pt.price)) continue - for (const i in tokens) sums[i][1] += pt.volumes[i] - } -} - -/* - * reorderAggregateData reorders the aggregated order book data so that the - * deepest books are first. - */ -function reorderAggregateData (response) { - let tokens = response.tokens - const sums = [] - for (const token of tokens) sums.push([token, 0]) - - const stats = orderbookStats(response.data.bids, response.data.asks) - - aggregateSums(response.data.bids, sums, tokens, v => v < stats.lowCut) - aggregateSums(response.data.asks, sums, tokens, v => v > stats.highCut) - - sums.sort((a, b) => a[1] - b[1]) - - const idxKey = {} - for (const i in sums) idxKey[sums[i][0]] = i - - const reorder = side => { - for (const pt of side) { - const v = [] - for (const i in pt.volumes) v[idxKey[tokens[i]]] = pt.volumes[i] - pt.volumes = v - } - } - reorder(response.data.bids) - reorder(response.data.asks) - - response.tokens = tokens = sums.map(v => v[0]) -} - -/* - * findAggregateDupes finds price bins in the aggregated depth chart data that - * have entries on both the buy and sell sides. Dygraphs doesn't handle the - * duplicates well during drawing, so we will try to clean up the Dygraphs data - * before passing it to the plotter. - */ -function findAggregateDupes (buys, sells) { - const dupes = [] - if (sells.length) { - let sellIdx = 0 - let sellPrice = sells[sellIdx][0] - - for (const i in buys) { - const buyPrice = buys[i][0] - if (buyPrice < sellPrice) continue - - while (buyPrice > sellPrice) { - sellIdx++ - if (sellIdx >= sells.length) return dupes - sellPrice = sells[sellIdx][0] - } - if (Math.round(buyPrice * 1e8) === Math.round(sellPrice * 1e8)) { - // Found a duplicate. - dupes.push({ - price: buyPrice, - i: buys.length + sellIdx, - buy: buys[i], - sell: sells[sellIdx] - }) - } - } - } - return dupes -} - -/* - * fixAggregateStacking attempts to correct a Dygraphs limitation where stacked - * plots don't display right when 1) the data isn't monotionically increasing in - * price, and 2) there is an exact match on price on the doubled back region. - */ -function fixAggregateStacking (e) { - if (e.setName.endsWith('buy')) return // only sell sides need fixing - const dupes = e.dygraph.getOption('dupes') - if (!dupes || dupes.length === 0) return - let dupeIdx = 0 - let dupe = dupes[dupeIdx] - // var dataIdx = e.seriesIndex + 1 - // var accume = 0 - // var accumeStacked = 0 - const pts = e.points - for (let i = dupe.i; i < pts.length; i++) { - const pt = pts[i] - if (dupe && i === dupe.i) { - // Need to adjust this one. Find a way to find a mapping from value to - // ratio to canvas position. - - // Figure out how much buy order is mistakenly added. - const misplacedVal = dupe.buy.reduce((acc, v) => { return i === 0 ? acc : acc + v }, 0) - const subRatio = misplacedVal / e.axis.maxyval - // Fixing these three values doesn't actually seem to affect the display, - // but fixing them anyway. - pt.y += subRatio - pt.y_stacked += subRatio - pt.yval_stacked -= misplacedVal - // This line is the ticket to remove the dark black outline on the spike. - pt.canvasy += subRatio * e.plotArea.h - - // TODO: Figure out how to add in missed accumulation, since the Dygraph - // bug seems to ignore the actual sell value. Or just dump Dygraphs and - // use canvas directly. - // accumeStacked += dupe.sell.reduce((acc, v) => { return i === 0 ? acc : acc + v}, 0) - // accume += dupe.sell[dataIdx] - - dupeIdx++ - if (dupeIdx >= dupes.length) dupe = null - else dupe = dupes[dupeIdx] - } - } -} diff --git a/cmd/dcrdata/public/scss/icons.scss b/cmd/dcrdata/public/scss/icons.scss index 09cfbafde..53b25a924 100644 --- a/cmd/dcrdata/public/scss/icons.scss +++ b/cmd/dcrdata/public/scss/icons.scss @@ -184,3 +184,7 @@ .exchange-logo.dcrdex { background-position: 0 -225px; } + +.exchange-logo.mexc { + background: url("/images/mexc-logo.svg") no-repeat; +} diff --git a/cmd/dcrdata/views/extras.tmpl b/cmd/dcrdata/views/extras.tmpl index 1330ffbc5..2af499ece 100644 --- a/cmd/dcrdata/views/extras.tmpl +++ b/cmd/dcrdata/views/extras.tmpl @@ -67,7 +67,7 @@ - + @@ -193,7 +193,7 @@ data-turbolinks-suppress-warning > diff --git a/cmd/dcrdata/views/market.tmpl b/cmd/dcrdata/views/market.tmpl index 04d2c2cb1..506564092 100644 --- a/cmd/dcrdata/views/market.tmpl +++ b/cmd/dcrdata/views/market.tmpl @@ -18,11 +18,14 @@ {{- /* PRICE */ -}}
-
1 DCR =
- {{if eq $botState.BtcIndex "USD"}} - $ - {{end}} - {{printf "%.2f" $botState.Price}} {{$botState.BtcIndex}} +
+ 1 DCR = + {{if eq $botState.Index "USD"}} + $ + {{end}} + {{printf "%.2f" $botState.Price}} + {{$botState.Index}} +
@@ -34,13 +37,17 @@ DCR Vol. - BTC + Price - {{$botState.BtcIndex}} + {{$botState.Index}} {{range $botState.VolumeOrderedExchanges}} - - {{xcDisplayName .Token}} + + + + {{xcDisplayName .Token}} + ({{.CurrencyPair.QuoteAsset}}) + {{threeSigFigs .State.Volume}} @@ -57,11 +64,11 @@ {{end}} - {{printf "%.2f" ($botState.BtcToFiat .State.Price)}} + {{printf "%.2f" ($botState.PriceToFiat .State.Price .CurrencyPair)}} {{end}} - + Aggregate {{threeSigFigs $botState.Volume}} @@ -83,13 +90,13 @@
Bitcoin Indices
- {{range $token, $state := $botState.FiatIndices}} + {{range $token, $state := $botState.BitcoinIndices}}
{{toTitleCase $token}}
- {{if eq $botState.BtcIndex "USD"}} + {{if eq $botState.Index "USD"}} $ {{end}} - {{commaWithDecimal $state.Price 2}} {{$botState.BtcIndex}}
+ {{commaWithDecimal $state.Price 2}} {{$botState.Index}}
{{if eq $token "coindesk"}} Powered by CoinDesk {{end}} @@ -102,7 +109,7 @@ {{- /* RIGHT COLUMN */ -}}
-
+
@@ -120,7 +127,7 @@ > {{range $botState.VolumeOrderedExchanges}} {{end}} -
@@ -178,23 +179,13 @@
- - -
- - {{- /* AGGREGATE DEPTH STACKING */ -}} -
- - - + +
{{- /* ZOOM */ -}} @@ -222,7 +213,7 @@ {{- /* CHART */ -}} -
+
diff --git a/exchanges/bot.go b/exchanges/bot.go index 9b4621f7b..39afcbc70 100644 --- a/exchanges/bot.go +++ b/exchanges/bot.go @@ -31,8 +31,7 @@ const ( defaultDCRRatesPort = "7778" - aggregatedOrderbookKey = "aggregated" - orderbookKey = "depth" + orderbookKey = "depth" ) // ExchangeBotConfig is the configuration options for ExchangeBot. @@ -43,7 +42,7 @@ type ExchangeBotConfig struct { Disabled []string DataExpiry string RequestExpiry string - BtcIndex string + Index string Indent bool MasterBot string MasterCertFile string @@ -54,17 +53,20 @@ type ExchangeBotConfig struct { // structures are prepared. Make ExchangeBot with NewExchangeBot. type ExchangeBot struct { mtx sync.RWMutex - DcrBtcExchanges map[string]Exchange + DcrExchanges map[string]Exchange IndexExchanges map[string]Exchange Exchanges map[string]Exchange versionedCharts map[string]*versionedChart chartVersions map[string]int - // BtcIndex is the (typically fiat) currency to which the DCR price should be + // Index is the (typically fiat) currency to which the DCR price should be // converted by default. Other conversions are available via a lookup in // indexMap, but with slightly lower performance. // 3-letter currency code, e.g. USD. - BtcIndex string - indexMap map[string]FiatIndices + Index string + // indexMap is a map of exchanges to supported indices for valid currencies + // like BTC and USDT or any other asset that is added for dcr in the future. + // New currency pairs must have at least one entry. + indexMap map[string]map[CurrencyPair]FiatIndices currentState ExchangeBotState // Both currentState and stateCopy hold the same information. currentState // is updated by ExchangeBot, and a copy stored in stateCopy. After creation, @@ -96,36 +98,60 @@ type ExchangeBot struct { // ExchangeBotState is the current known state of all exchanges, in a certain // base currency, and a volume-averaged price and total volume in DCR. type ExchangeBotState struct { - BtcIndex string `json:"btc_index"` - BtcPrice float64 `json:"btc_fiat_price"` - Price float64 `json:"price"` - Volume float64 `json:"volume"` - DcrBtc map[string]*ExchangeState `json:"dcr_btc_exchanges"` + Index string `json:"index"` + BtcPrice float64 `json:"btc_fiat_price"` + Price float64 `json:"price"` + Volume float64 `json:"volume"` + DCRExchanges map[string]map[CurrencyPair]*ExchangeState `json:"dcr_exchanges"` // FiatIndices: // TODO: We only really need the BaseState for the fiat indices. - FiatIndices map[string]*ExchangeState `json:"btc_indices"` + FiatIndices map[string]map[CurrencyPair]*ExchangeState `json:"indices"` } // Copy an ExchangeState map. -func copyStates(m map[string]*ExchangeState) map[string]*ExchangeState { - c := make(map[string]*ExchangeState) - for k, v := range m { - c[k] = v +func copyStates(m map[string]map[CurrencyPair]*ExchangeState) map[string]map[CurrencyPair]*ExchangeState { + c := make(map[string]map[CurrencyPair]*ExchangeState) + for t, v := range m { + mc := make(map[CurrencyPair]*ExchangeState) + for p, s := range v { + mc[p] = s + } + c[t] = mc } return c } // Creates a pointer to a copy of the ExchangeBotState. func (state ExchangeBotState) copy() *ExchangeBotState { - state.DcrBtc = copyStates(state.DcrBtc) + state.DCRExchanges = copyStates(state.DCRExchanges) state.FiatIndices = copyStates(state.FiatIndices) return &state } -// BtcToFiat converts an amount of Bitcoin to fiat using the current calculated -// exchange rate. -func (state *ExchangeBotState) BtcToFiat(btc float64) float64 { - return state.BtcPrice * btc +// BtcToFiat converts an amount of {Bitcoin, USDT} to fiat using the current +// calculated exchange rate. +func (state *ExchangeBotState) PriceToFiat(price float64, currencyPair CurrencyPair) float64 { + switch currencyPair { + case CurrencyPairDCRBTC: + return state.BtcPrice * price + + case CurrencyPairDCRUSDT: + var usdtPrice, nSources float64 + for _, currencyIndices := range state.FiatIndices { + state := currencyIndices[USDTIndex] + if state != nil { + usdtPrice += state.Price + nSources++ + } + } + if usdtPrice != 0 { + usdtPrice = usdtPrice / nSources + } + return usdtPrice * price + + default: + return 0 + } } // FiatToBtc converts an amount of fiat in the default index to a value in BTC. @@ -136,22 +162,74 @@ func (state *ExchangeBotState) FiatToBtc(fiat float64) float64 { return fiat / state.BtcPrice } +// BitcoinIndices returns a map of all exchanges that provide a bitcoin index. +func (state *ExchangeBotState) BitcoinIndices() map[string]BaseState { + fiatIndices := make(map[string]BaseState) + for token, states := range state.FiatIndices { + s := states[BTCIndex] + if s != nil { + fiatIndices[token] = s.BaseState + } + } + return fiatIndices +} + +// Indices returns a map of known indices to their current fiat price. Returns +// an empty json object ({}) if it encounters an error. +func (state *ExchangeBotState) Indices() string { + sumIndexPrice := func(index CurrencyPair) float64 { + var price, nSource float64 + for _, states := range state.FiatIndices { + s := states[index] + if s != nil && s.Price > 0 { + price += s.Price + nSource++ + } + } + if price == 0 { + return 0 + } + return price / nSource + } + + fiatIndices := make(map[string]float64) + for _, states := range state.FiatIndices { + for i := range states { + if _, found := fiatIndices[i.String()]; !found { + fiatIndices[i.String()] = sumIndexPrice(i) + } + } + } + + b, err := json.Marshal(fiatIndices) + if err != nil { + log.Errorf("ExchangeBotState.Indices: json.Marshal error: %v", err) + return "{}" + } + + return string(b) +} + // ExchangeState doesn't have a Token field, so if the states are returned as a // slice (rather than ranging over a map), a token is needed. type tokenedExchange struct { Token string + CurrencyPair State *ExchangeState } // VolumeOrderedExchanges returns a list of tokenedExchange sorted by volume, // highest volume first. func (state *ExchangeBotState) VolumeOrderedExchanges() []*tokenedExchange { - xcList := make([]*tokenedExchange, 0, len(state.DcrBtc)) - for token, state := range state.DcrBtc { - xcList = append(xcList, &tokenedExchange{ - Token: token, - State: state, - }) + var xcList []*tokenedExchange + for token, states := range state.DCRExchanges { + for pair, state := range states { + xcList = append(xcList, &tokenedExchange{ + Token: token, + CurrencyPair: pair, + State: state, + }) + } } sort.Slice(xcList, func(i, j int) bool { return xcList[i].State.Volume > xcList[j].State.Volume @@ -159,46 +237,15 @@ func (state *ExchangeBotState) VolumeOrderedExchanges() []*tokenedExchange { return xcList } -// A price bin for the aggregated orderbook. The Volumes array will be length -// N = number of depth-reporting exchanges. If any exchange has an order book -// entry at price Price, then an agBookPt should be created. If a different -// exchange does not have an order at Price, there will be a 0 in Volumes at -// the exchange's index. An exchange's index in Volumes is set by its index -// in (aggregateOrderbook).Tokens. -type agBookPt struct { - Price float64 `json:"price"` - Volumes []float64 `json:"volumes"` -} - -// The aggregated depth data. Similar to DepthData, but with agBookPts instead. -// For aggregateData, the Time will indicate the most recent time at which an -// exchange with non-nil DepthData was updated. -type aggregateData struct { - Time int64 `json:"time"` - Bids []agBookPt `json:"bids"` - Asks []agBookPt `json:"asks"` -} - -// An aggregated orderbook. Combines all data from the DepthData of each -// Exchange. For aggregatedOrderbook, the Expiration is set to the time of the -// most recent DepthData update plus an additional (ExchangeBot).RequestExpiry, -// though new data may be available before then. -type aggregateOrderbook struct { - BtcIndex string `json:"btc_index"` - Price float64 `json:"price"` - Tokens []string `json:"tokens"` - UpdateTimes []int64 `json:"update_times"` - Data aggregateData `json:"data"` - Expiration int64 `json:"expiration"` -} - -// FiatIndices maps currency codes to Bitcoin exchange rates. +// FiatIndices maps currency codes to an asset's exchange rates, e.g +// Bitcoin-USD etc. type FiatIndices map[string]float64 // IndexUpdate is sent from the Exchange to the ExchangeBot indexChan when new // data is received. type IndexUpdate struct { - Token string + Token string + CurrencyPair Indices FiatIndices } @@ -219,7 +266,7 @@ type UpdateChannels struct { // The chart data structures that are encoded and cached are the // candlestickResponse and the depthResponse. type candlestickResponse struct { - BtcIndex string `json:"index"` + Index string `json:"index"` Price float64 `json:"price"` Sticks Candlesticks `json:"sticks"` Expiration int64 `json:"expiration"` @@ -267,24 +314,24 @@ func NewExchangeBot(config *ExchangeBotConfig) (*ExchangeBot, error) { if dataExpiry < time.Minute { return nil, fmt.Errorf("Expiration must be at least one minute") } - if config.BtcIndex == "" { - config.BtcIndex = DefaultCurrency + if config.Index == "" { + config.Index = DefaultCurrency } bot := &ExchangeBot{ - DcrBtcExchanges: make(map[string]Exchange), + DcrExchanges: make(map[string]Exchange), IndexExchanges: make(map[string]Exchange), Exchanges: make(map[string]Exchange), versionedCharts: make(map[string]*versionedChart), chartVersions: make(map[string]int), - BtcIndex: config.BtcIndex, - indexMap: make(map[string]FiatIndices), + Index: config.Index, + indexMap: make(map[string]map[CurrencyPair]FiatIndices), currentState: ExchangeBotState{ - BtcIndex: config.BtcIndex, - Price: 0, - Volume: 0, - DcrBtc: make(map[string]*ExchangeState), - FiatIndices: make(map[string]*ExchangeState), + Index: config.Index, + Price: 0, + Volume: 0, + DCRExchanges: make(map[string]map[CurrencyPair]*ExchangeState), + FiatIndices: make(map[string]map[CurrencyPair]*ExchangeState), }, currentStateBytes: []byte{}, DataExpiry: dataExpiry, @@ -353,20 +400,20 @@ func NewExchangeBot(config *ExchangeBotConfig) (*ExchangeBot, error) { bot.Exchanges[token] = xc } - for token, constructor := range BtcIndices { + for token, constructor := range Indices { buildExchange(token, constructor, bot.IndexExchanges) } for token, constructor := range DcrExchanges { - buildExchange(token, constructor, bot.DcrBtcExchanges) + buildExchange(token, constructor, bot.DcrExchanges) } - if len(bot.DcrBtcExchanges) == 0 { - return nil, fmt.Errorf("no DCR-BTC exchanges were initialized") + if len(bot.DcrExchanges) == 0 { + return nil, fmt.Errorf("no DCR exchanges were initialized") } if len(bot.IndexExchanges) == 0 { - return nil, fmt.Errorf("no BTC-fiat exchanges were initialized") + return nil, fmt.Errorf("no {BTC, USDT}-fiat exchanges were initialized") } return bot, nil @@ -422,13 +469,22 @@ func (bot *ExchangeBot) Start(ctx context.Context, wg *sync.WaitGroup) { reconnectionAttempt = 0 continue } - // Send the update through the Exchange so that appropriate attributes - // are set. + // Send the update through the Exchange so that appropriate + // attributes are set. if IsDcrExchange(update.Token) { - state := exchangeStateFromProto(update) - bot.Exchanges[update.Token].Update(state) - } else if IsBtcIndex(update.Token) { - bot.Exchanges[update.Token].UpdateIndices(update.GetIndices()) + currencyPair, state := exchangeStateFromProto(update) + if !currencyPair.IsValidDCRPair() { + log.Errorf("Received update for unknown currency pair %s", currencyPair) + } else { + bot.Exchanges[update.Token].Update(currencyPair, state) + } + } else if IsIndex(update.Token) { + currencyIndex := CurrencyPair(update.GetCurrencyPair()) + if !currencyIndex.IsValidIndex() { + log.Errorf("Received update for unknown index %s", currencyIndex) + } else { + bot.Exchanges[update.Token].UpdateIndices(currencyIndex, update.GetIndices()) + } } } }() @@ -454,7 +510,7 @@ out: for { select { case update := <-bot.exchangeChan: - log.Tracef("exchange update received from %s with a BTC price %f, ", update.Token, update.State.Price) + log.Tracef("exchange update received from %s (Currency Pair: %s) with price %f, ", update.Token, update.CurrencyPair, update.State.Price) err := bot.updateExchange(update) if err != nil { log.Warnf("Error encountered in exchange update: %v", err) @@ -462,9 +518,9 @@ out: } bot.signalExchangeUpdate(update) case update := <-bot.indexChan: - btcPrice, found := update.Indices[bot.BtcIndex] + price, found := update.Indices[bot.Index] if found { - log.Tracef("index update received from %s with %d indices, %s price for Bitcoin is %f", update.Token, len(update.Indices), bot.BtcIndex, btcPrice) + log.Tracef("index update received from %s with %d indices, %s price for %s is %f", update.Token, len(update.Indices), bot.Index, update.CurrencyPair, price) } err := bot.updateIndices(update) if err != nil { @@ -515,7 +571,7 @@ func (bot *ExchangeBot) connectMasterBot(ctx context.Context, delay time.Duratio bot.masterConnection = conn grpcClient := dcrrates.NewDCRRatesClient(conn) stream, err := grpcClient.SubscribeExchanges(ctx, &dcrrates.ExchangeSubscription{ - BtcIndex: bot.BtcIndex, + Index: bot.Index, Exchanges: bot.subscribedExchanges(), }) if err != nil { @@ -583,34 +639,42 @@ func (bot *ExchangeBot) State() *ExchangeBotState { return bot.stateCopy } -// ConvertedState returns an ExchangeBotState with a base of the provided -// currency code, if available. -func (bot *ExchangeBot) ConvertedState(code string) (*ExchangeBotState, error) { - bot.mtx.RLock() - defer bot.mtx.RUnlock() - fiatIndices := make(map[string]*ExchangeState) +// indicesForCode must be called under bot.mtx lock. +func (bot *ExchangeBot) indicesForCode(code string) map[string]map[CurrencyPair]*ExchangeState { + fiatIndices := make(map[string]map[CurrencyPair]*ExchangeState) for token, indices := range bot.indexMap { - for symbol, price := range indices { - if symbol == code { - fiatIndices[token] = &ExchangeState{BaseState: BaseState{Price: price}} + for currencyPair, indice := range indices { + for symbol, price := range indice { + if symbol == code { + fiatIndices[token] = map[CurrencyPair]*ExchangeState{ + currencyPair: {BaseState: BaseState{Price: price}}, + } + } } } } + return fiatIndices +} - dcrPrice, volume := bot.processState(bot.currentState.DcrBtc, true) - btcPrice, _ := bot.processState(fiatIndices, false) +// ConvertedState returns an ExchangeBotState with a base of the provided +// currency code, if available. +func (bot *ExchangeBot) ConvertedState(code string) (*ExchangeBotState, error) { + bot.mtx.RLock() + defer bot.mtx.RUnlock() + dcrPrice, volume := bot.dcrPriceAndVolume(code) + btcPrice := bot.indexPrice(BTCIndex, code) if dcrPrice == 0 || btcPrice == 0 { bot.failed = true return nil, fmt.Errorf("Unable to process price for currency %s", code) } state := ExchangeBotState{ - BtcIndex: code, - Volume: volume * btcPrice, - Price: dcrPrice * btcPrice, - BtcPrice: btcPrice, - DcrBtc: bot.currentState.DcrBtc, - FiatIndices: fiatIndices, + Index: code, + Volume: volume, + Price: dcrPrice, + BtcPrice: dcrPrice, + DCRExchanges: bot.currentState.DCRExchanges, + FiatIndices: bot.indicesForCode(code), } return state.copy(), nil @@ -618,10 +682,10 @@ func (bot *ExchangeBot) ConvertedState(code string) (*ExchangeBotState, error) { // ExchangeRates is the dcr and btc prices converted to fiat. type ExchangeRates struct { - BtcIndex string `json:"btcIndex"` - DcrPrice float64 `json:"dcrPrice"` - BtcPrice float64 `json:"btcPrice"` - Exchanges map[string]BaseState `json:"exchanges"` + Index string `json:"index"` + DcrPrice float64 `json:"dcrPrice"` + BtcPrice float64 `json:"btcPrice"` + Exchanges map[string]map[CurrencyPair]BaseState `json:"exchanges"` } // Rates is the current exchange rates for dcr and btc. @@ -630,16 +694,20 @@ func (bot *ExchangeBot) Rates() *ExchangeRates { defer bot.mtx.RUnlock() s := bot.stateCopy - xcs := make(map[string]BaseState, len(s.DcrBtc)) - for token, xcState := range s.DcrBtc { - xcs[token] = xcState.BaseState + xcMarkets := make(map[string]map[CurrencyPair]BaseState, len(s.DCRExchanges)) + for token, xcStates := range s.DCRExchanges { + xcs := make(map[CurrencyPair]BaseState, len(xcStates)) + for currencyPair, xcState := range xcStates { + xcs[currencyPair] = xcState.BaseState + } + xcMarkets[token] = xcs } return &ExchangeRates{ - BtcIndex: s.BtcIndex, + Index: s.Index, DcrPrice: s.Price, BtcPrice: s.BtcPrice, - Exchanges: xcs, + Exchanges: xcMarkets, } } @@ -648,25 +716,16 @@ func (bot *ExchangeBot) Rates() *ExchangeRates { func (bot *ExchangeBot) ConvertedRates(code string) (*ExchangeRates, error) { bot.mtx.RLock() defer bot.mtx.RUnlock() - fiatIndices := make(map[string]*ExchangeState) - for token, indices := range bot.indexMap { - for symbol, price := range indices { - if symbol == code { - fiatIndices[token] = &ExchangeState{BaseState: BaseState{Price: price}} - } - } - } - - dcrPrice, _ := bot.processState(bot.currentState.DcrBtc, true) - btcPrice, _ := bot.processState(fiatIndices, false) - if dcrPrice == 0 || btcPrice == 0 { + dcrPrice, _ := bot.dcrPriceAndVolume(code) + btcPrice := bot.indexPrice(BTCIndex, code) + if btcPrice == 0 || dcrPrice == 0 { bot.failed = true return nil, fmt.Errorf("Unable to process price for currency %s", code) } return &ExchangeRates{ - BtcIndex: code, - DcrPrice: dcrPrice * btcPrice, + Index: code, + DcrPrice: dcrPrice, BtcPrice: btcPrice, }, nil } @@ -710,21 +769,23 @@ func (bot *ExchangeBot) AvailableIndices() []string { indices = append(indices, index) } for _, fiatIndices := range bot.indexMap { - for symbol := range fiatIndices { - add(symbol) + for _, indices := range fiatIndices { + for symbol := range indices { + add(symbol) + } } } sort.Sort(indices) return indices } -// Indices is the fiat indices for a given BTC index exchange. -func (bot *ExchangeBot) Indices(token string) FiatIndices { +// Indices is the fiat indices for a given {BTC, USDT} index exchange. +func (bot *ExchangeBot) Indices(token string) map[CurrencyPair]FiatIndices { bot.mtx.RLock() defer bot.mtx.RUnlock() - indices := make(FiatIndices) - for code, price := range bot.indexMap[token] { - indices[code] = price + indices := make(map[CurrencyPair]FiatIndices) + for currencyIndex, indice := range bot.indexMap[token] { + indices[currencyIndex] = indice } return indices } @@ -746,60 +807,99 @@ func (bot *ExchangeBot) cachedChartVersion(chartId string) int { return cid } -// processState is a helper function to process a slice of ExchangeState into -// a price, and optionally a volume sum, and perform some cleanup along the way. +// processState is a helper function to process a slice of ExchangeState into a +// price, and optionally a volume sum, and perform some cleanup along the way. // If volumeAveraged is false, all exchanges are given equal weight in the avg. -func (bot *ExchangeBot) processState(states map[string]*ExchangeState, volumeAveraged bool) (float64, float64) { - var priceAccumulator, volSum float64 - var deletions []string +// If exchange is invalid, a bool false is returned as a last return value. +func (bot *ExchangeBot) processState(token, code string, states map[CurrencyPair]*ExchangeState, volumeAveraged bool) (float64, float64, bool) { oldestValid := time.Now().Add(-bot.RequestExpiry) - for token, state := range states { - if bot.Exchanges[token].LastUpdate().Before(oldestValid) { - deletions = append(deletions, token) - continue - } + if bot.Exchanges[token].LastUpdate().Before(oldestValid) { + return 0, 0, false + } + + var priceAccumulator, volSum float64 + for currencyPair, state := range states { volume := 1.0 if volumeAveraged { volume = state.Volume } volSum += volume - priceAccumulator += volume * state.Price - } - for _, token := range deletions { - delete(states, token) + + // Convert price to bot.Index. + price := state.Price + switch currencyPair { + case CurrencyPairDCRBTC: + price = bot.indexPrice(BTCIndex, code) * price + case CurrencyPairDCRUSDT: + price = bot.indexPrice(USDTIndex, code) * price + } + if price == 0 { // missing index price for currencyPair. + return 0, 0, false + } + + priceAccumulator += volume * price } + if volSum == 0 { - return 0, 0 + return 0, 0, true } - return priceAccumulator / volSum, volSum + return priceAccumulator / volSum, volSum, true } -// updateExchange processes an update from a Decred-BTC Exchange. +// indexPrice retrieves the index price for the provided currency index +// {BTC-Index, USDT-Index}. Must be called under bot.mutex lock. +func (bot *ExchangeBot) indexPrice(index CurrencyPair, code string) float64 { + var price, nSource float64 + for _, currencyIndex := range bot.indexMap { + indices := currencyIndex[index] + if len(indices) != 0 && indices[code] > 0 { + price += indices[code] + nSource++ + } + } + if price == 0 { + return 0 + } + return price / nSource +} + +// updateExchange processes an update from a Decred-{Asset} Exchange. func (bot *ExchangeBot) updateExchange(update *ExchangeUpdate) error { bot.mtx.Lock() defer bot.mtx.Unlock() if update.State.Candlesticks != nil { for bin := range update.State.Candlesticks { - bot.incrementChart(genCacheID(update.Token, string(bin))) + bot.incrementChart(genCacheID(update.CurrencyPair.String(), update.Token, string(bin))) } } if update.State.Depth != nil { - bot.incrementChart(genCacheID(update.Token, orderbookKey)) - bot.incrementChart(genCacheID(aggregatedOrderbookKey, orderbookKey)) + bot.incrementChart(genCacheID(update.CurrencyPair.String(), update.Token, orderbookKey)) + } + + if bot.currentState.DCRExchanges[update.Token] == nil { + bot.currentState.DCRExchanges[update.Token] = make(map[CurrencyPair]*ExchangeState) } - bot.currentState.DcrBtc[update.Token] = update.State + bot.currentState.DCRExchanges[update.Token][update.CurrencyPair] = update.State return bot.updateState() } -// updateIndices processes an update from an Bitcoin index source, essentially -// a map pairing currency codes to bitcoin prices. +// updateIndices processes an update from an {Bitcoin, USDT} index source, +// essentially a map pairing currency codes to bitcoin or usdt prices. func (bot *ExchangeBot) updateIndices(update *IndexUpdate) error { bot.mtx.Lock() defer bot.mtx.Unlock() - bot.indexMap[update.Token] = update.Indices - price, hasCode := update.Indices[bot.config.BtcIndex] + if bot.indexMap[update.Token] == nil { + bot.indexMap[update.Token] = make(map[CurrencyPair]FiatIndices) + } + + bot.indexMap[update.Token][update.CurrencyPair] = update.Indices + price, hasCode := update.Indices[bot.config.Index] if hasCode { - bot.currentState.FiatIndices[update.Token] = &ExchangeState{ + if bot.currentState.FiatIndices[update.Token] == nil { + bot.currentState.FiatIndices[update.Token] = make(map[CurrencyPair]*ExchangeState) + } + + bot.currentState.FiatIndices[update.Token][update.CurrencyPair] = &ExchangeState{ BaseState: BaseState{ Price: price, Stamp: time.Now().Unix(), @@ -807,19 +907,44 @@ func (bot *ExchangeBot) updateIndices(update *IndexUpdate) error { } return bot.updateState() } - log.Warnf("Default currency code, %s, not contained in update from %s", bot.BtcIndex, update.Token) + log.Warnf("Default currency code, %s, not contained in update from %s", bot.Index, update.Token) return nil } +// dcrPriceAndVolume calculates and returns dcr price and volume. The returned +// dcr price is converted to the provided index code. Must be called under +// bot.mtx lock. +func (bot *ExchangeBot) dcrPriceAndVolume(code string) (float64, float64) { + var dcrPrice, volume, nSources float64 + for token, xcStates := range bot.currentState.DCRExchanges { + processedDcrPrice, processedVolume, ok := bot.processState(token, code, xcStates, true) + if !ok { + continue + } + + volume += processedVolume + if processedDcrPrice != 0 { + dcrPrice += processedDcrPrice + nSources++ + } + } + + if dcrPrice == 0 { + return 0, 0 + } + + return dcrPrice / nSources, volume +} + // Called from both updateIndices and updateExchange (under mutex lock). func (bot *ExchangeBot) updateState() error { - dcrPrice, volume := bot.processState(bot.currentState.DcrBtc, true) - btcPrice, _ := bot.processState(bot.currentState.FiatIndices, false) - if dcrPrice == 0 || btcPrice == 0 { + btcPrice := bot.indexPrice(BTCIndex, bot.Index) + dcrPrice, volume := bot.dcrPriceAndVolume(bot.Index) + if btcPrice == 0 || dcrPrice == 0 { bot.failed = true } else { bot.failed = false - bot.currentState.Price = dcrPrice * btcPrice + bot.currentState.Price = dcrPrice bot.currentState.BtcPrice = btcPrice bot.currentState.Volume = volume } @@ -880,7 +1005,7 @@ func (bot *ExchangeBot) Cycle() { } } -// Price gets the lastest Price in the default currency (BtcIndex). +// Price gets the latest Price in the default currency (Index). func (bot *ExchangeBot) Price() float64 { bot.mtx.RLock() defer bot.mtx.RUnlock() @@ -915,7 +1040,7 @@ func (bot *ExchangeBot) Conversion(dcrVal float64) *Conversion { if xcState != nil { return &Conversion{ Value: xcState.Price * dcrVal, - Index: xcState.BtcIndex, + Index: xcState.Index, } } // Haven't gotten data yet, but we're running. @@ -939,8 +1064,12 @@ func (bot *ExchangeBot) fetchFromCache(chartID string) (data []byte, bestVersion // QuickSticks returns the up-to-date candlestick data for the specified // exchange and bin width, pulling from the cache if appropriate. -func (bot *ExchangeBot) QuickSticks(token string, rawBin string) ([]byte, error) { - chartID := genCacheID(token, rawBin) +func (bot *ExchangeBot) QuickSticks(token string, market CurrencyPair, rawBin string) ([]byte, error) { + if !market.IsValidDCRPair() { + return nil, fmt.Errorf("invalid market %s", market) + } + + chartID := genCacheID(market.String(), token, rawBin) bin := candlestickKey(rawBin) data, bestVersion, isGood := bot.fetchFromCache(chartID) if isGood { @@ -951,10 +1080,15 @@ func (bot *ExchangeBot) QuickSticks(token string, rawBin string) ([]byte, error) bot.mtx.Lock() defer bot.mtx.Unlock() - state, found := bot.currentState.DcrBtc[token] + xcStates, found := bot.currentState.DCRExchanges[token] if !found { return nil, fmt.Errorf("Failed to find DCR exchange state for %s", token) } + + state, found := xcStates[market] + if !found { + return nil, fmt.Errorf("Failed to find DCR exchange state for %s (Currency Pair: %s)", token, market) + } if state.Candlesticks == nil { return nil, fmt.Errorf("Failed to find candlesticks for %s", token) } @@ -968,9 +1102,8 @@ func (bot *ExchangeBot) QuickSticks(token string, rawBin string) ([]byte, error) } expiration := sticks[len(sticks)-1].Start.Add(2 * bin.duration()) - chart, err := bot.encodeJSON(&candlestickResponse{ - BtcIndex: bot.BtcIndex, + Index: bot.Index, Price: bot.currentState.Price, Sticks: sticks, Expiration: expiration.Unix(), @@ -989,130 +1122,37 @@ func (bot *ExchangeBot) QuickSticks(token string, rawBin string) ([]byte, error) return vChart.chart, nil } -// Move the DepthPoint array into a map whose entries are agBookPt, inserting -// the (DepthPoint).Quantity values at xcIndex of Volumes. Creates Volumes -// if it does not yet exist. -func mapifyDepthPoints(source []DepthPoint, target map[int64]agBookPt, xcIndex, ptCount int) { - for _, pt := range source { - k := eightPtKey(pt.Price) - _, found := target[k] - if !found { - target[k] = agBookPt{ - Price: pt.Price, - Volumes: make([]float64, ptCount), - } - } - target[k].Volumes[xcIndex] = pt.Quantity +// QuickDepth returns the up-to-date depth chart data for the specified exchange +// market, pulling from the cache if appropriate. +func (bot *ExchangeBot) QuickDepth(token string, market CurrencyPair) (chart []byte, err error) { + if !market.IsValidDCRPair() { + return nil, fmt.Errorf("invalid market %s", market) } -} - -// A list of eightPtKey keys from an orderbook tracking map. Used for sorting. -func agBookMapKeys(book map[int64]agBookPt) []int64 { - keys := make([]int64, 0, len(book)) - for k := range book { - keys = append(keys, k) - } - return keys -} - -// After the aggregate orderbook map is fully assembled, sort the keys and -// process the map into a list of lists. -func unmapAgOrders(book map[int64]agBookPt, reverse bool) []agBookPt { - orderedBook := make([]agBookPt, 0, len(book)) - keys := agBookMapKeys(book) - if reverse { - sort.Slice(keys, func(i, j int) bool { return keys[j] < keys[i] }) - } else { - sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) - } - for _, k := range keys { - orderedBook = append(orderedBook, book[k]) - } - return orderedBook -} - -// Make an aggregate orderbook from all depth data. -func (bot *ExchangeBot) aggOrderbook() *aggregateOrderbook { - state := bot.State() - if state == nil { - return nil - } - bids := make(map[int64]agBookPt) - asks := make(map[int64]agBookPt) - - oldestUpdate := time.Now().Unix() - var newestTime int64 - // First, grab the tokens for exchanges with depth data so that they can be - // counted and sorted alphabetically. - tokens := []string{} - for token, xcState := range state.DcrBtc { - if !xcState.HasDepth() { - continue - } - tokens = append(tokens, token) - } - numXc := len(tokens) - updateTimes := make([]int64, 0, numXc) - sort.Strings(tokens) - for i, token := range tokens { - xcState := state.DcrBtc[token] - depth := xcState.Depth - if depth.Time < oldestUpdate { - oldestUpdate = depth.Time - } - if depth.Time > newestTime { - newestTime = depth.Time - } - updateTimes = append(updateTimes, depth.Time) - mapifyDepthPoints(depth.Bids, bids, i, numXc) - mapifyDepthPoints(depth.Asks, asks, i, numXc) - } - return &aggregateOrderbook{ - Tokens: tokens, - BtcIndex: bot.BtcIndex, - Price: state.Price, - UpdateTimes: updateTimes, - Data: aggregateData{ - Time: newestTime, - Bids: unmapAgOrders(bids, true), - Asks: unmapAgOrders(asks, false), - }, - Expiration: oldestUpdate + int64(bot.RequestExpiry.Seconds()), - } -} -// QuickDepth returns the up-to-date depth chart data for the specified -// exchange, pulling from the cache if appropriate. -func (bot *ExchangeBot) QuickDepth(token string) (chart []byte, err error) { - chartID := genCacheID(token, orderbookKey) + chartID := genCacheID(market.String(), token, orderbookKey) data, bestVersion, isGood := bot.fetchFromCache(chartID) if isGood { return data, nil } - if token == aggregatedOrderbookKey { - agDepth := bot.aggOrderbook() - if agDepth == nil { - return nil, fmt.Errorf("Failed to find depth for %s", token) - } - chart, err = bot.encodeJSON(agDepth) - } else { - bot.mtx.Lock() - defer bot.mtx.Unlock() - xcState, found := bot.currentState.DcrBtc[token] - if !found { - return nil, fmt.Errorf("Failed to find DCR exchange state for %s", token) - } - if xcState.Depth == nil { - return nil, fmt.Errorf("Failed to find depth for %s", token) - } - chart, err = bot.encodeJSON(&depthResponse{ - BtcIndex: bot.BtcIndex, - Price: bot.currentState.Price, - Data: xcState.Depth, - Expiration: xcState.Depth.Time + int64(bot.RequestExpiry.Seconds()), - }) + bot.mtx.Lock() + defer bot.mtx.Unlock() + xcStates, found := bot.currentState.DCRExchanges[token] + if !found { + return nil, fmt.Errorf("Failed to find DCR exchange state for %s (Currency Pair: %s)", token, market) + } + + state, ok := xcStates[market] + if !ok || state.Depth == nil { + return nil, fmt.Errorf("Failed to find depth for %s (Currency Pair: %s)", token, market) } + + chart, err = bot.encodeJSON(&depthResponse{ + BtcIndex: bot.Index, + Price: bot.currentState.Price, + Data: state.Depth, + Expiration: state.Depth.Time + int64(bot.RequestExpiry.Seconds()), + }) if err != nil { return nil, fmt.Errorf("JSON encode error for %s depth chart", token) } diff --git a/exchanges/exchanges.go b/exchanges/exchanges.go index 88c73d294..8c202580d 100644 --- a/exchanges/exchanges.go +++ b/exchanges/exchanges.go @@ -34,13 +34,13 @@ const ( Huobi = "huobi" Poloniex = "poloniex" DexDotDecred = "dcrdex" + Mexc = "mexc" ) // A few candlestick bin sizes. type candlestickKey string const ( - fiveMinKey candlestickKey = "5m" halfHourKey candlestickKey = "30m" hourKey candlestickKey = "1h" dayKey candlestickKey = "1d" @@ -48,7 +48,6 @@ const ( ) var candlestickDurations = map[candlestickKey]time.Duration{ - fiveMinKey: time.Minute * 5, halfHourKey: time.Minute * 30, hourKey: time.Hour, dayKey: time.Hour * 24, @@ -64,12 +63,47 @@ func (k candlestickKey) duration() time.Duration { return d } +// CurrencyPair is any currency pair, e.g DCR-{Asset} or currency index, e.g +// BTC-Index, USDT-Index. +type CurrencyPair string + +const ( + CurrencyPairDCRBTC CurrencyPair = "DCR-BTC" + CurrencyPairDCRUSDT CurrencyPair = "DCR-USDT" + + // BTCIndex is an index pair and not a valid DCR-{Asset} market. + BTCIndex CurrencyPair = "BTC-Index" + USDTIndex CurrencyPair = "USDT-Index" +) + +func (cp CurrencyPair) IsValidDCRPair() bool { + return cp == CurrencyPairDCRBTC || cp == CurrencyPairDCRUSDT +} + +func (cp CurrencyPair) IsValidIndex() bool { + return cp == BTCIndex || cp == USDTIndex +} + +func (cp CurrencyPair) QuoteAsset() string { + if !cp.IsValidDCRPair() { + return cp.String() + } + + v := strings.Split(cp.String(), "-") + return strings.ToTitle(v[1]) +} + +func (cp CurrencyPair) String() string { + return string(cp) +} + // URLs is a set of endpoints for an exchange's various datasets. type URLs struct { - Price string - Stats string - Depth string - Candlesticks map[candlestickKey]string + Markets []CurrencyPair + Price map[CurrencyPair]string + Stats map[CurrencyPair]string + Depth map[CurrencyPair]string + Candlesticks map[CurrencyPair]map[candlestickKey]string Websocket string } @@ -80,82 +114,156 @@ type requests struct { candlesticks map[candlestickKey]*http.Request } -func newRequests() requests { - return requests{ - candlesticks: make(map[candlestickKey]*http.Request), +func newRequests(markets []CurrencyPair) map[CurrencyPair]*requests { + reqs := make(map[CurrencyPair]*requests, len(markets)) + for _, mkt := range markets { + reqs[mkt] = &requests{ + candlesticks: make(map[candlestickKey]*http.Request), + } } + return reqs } // Prepare the URLs. var ( CoinbaseURLs = URLs{ - Price: "https://api.coinbase.com/v2/exchange-rates?currency=BTC", + Markets: []CurrencyPair{BTCIndex, USDTIndex}, + Price: map[CurrencyPair]string{ + BTCIndex: "https://api.coinbase.com/v2/exchange-rates?currency=BTC", + USDTIndex: "https://api.coinbase.com/v2/exchange-rates?currency=USDT", + }, } CoindeskURLs = URLs{ - Price: "https://api.coindesk.com/v2/bpi/currentprice.json", + Markets: []CurrencyPair{BTCIndex}, + Price: map[CurrencyPair]string{ + BTCIndex: "https://api.coindesk.com/v2/bpi/currentprice.json", + }, + } + // https://api.mexc.com/api/v3/depth?symbol=DCRUSDT + MexcURLs = URLs{ + Markets: []CurrencyPair{CurrencyPairDCRUSDT}, + Price: map[CurrencyPair]string{ + CurrencyPairDCRUSDT: "https://api.mexc.com/api/v3/ticker/24hr?symbol=DCRUSDT", + }, + Depth: map[CurrencyPair]string{ + // Mexc returns a maximum of 5000 depth chart points. This seems + // like it is the entire order book at least sometimes. + CurrencyPairDCRUSDT: "https://api.mexc.com/api/v3/depth?symbol=DCRUSDT&limit=5000", + }, + Candlesticks: map[CurrencyPair]map[candlestickKey]string{ + CurrencyPairDCRUSDT: { + // 1000 is the maximum sticks returned. + hourKey: "https://api.mexc.com/api/v3/klines?symbol=DCRUSDT&limit=1000&interval=60m", + dayKey: "https://api.mexc.com/api/v3/klines?symbol=DCRUSDT&limit=1000&interval=1d", + monthKey: "https://api.mexc.com/api/v3/klines?symbol=DCRUSDT&limit=1000&interval=1M", + }, + }, } BinanceURLs = URLs{ - Price: "https://api.binance.com/api/v3/ticker/24hr?symbol=DCRBTC", - // Binance returns a maximum of 5000 depth chart points. This seems like it - // is the entire order book at least sometimes. - Depth: "https://api.binance.com/api/v3/depth?symbol=DCRBTC&limit=5000", - Candlesticks: map[candlestickKey]string{ - hourKey: "https://api.binance.com/api/v3/klines?symbol=DCRBTC&interval=1h", - dayKey: "https://api.binance.com/api/v3/klines?symbol=DCRBTC&interval=1d", - monthKey: "https://api.binance.com/api/v3/klines?symbol=DCRBTC&interval=1M", + Markets: []CurrencyPair{CurrencyPairDCRBTC, CurrencyPairDCRUSDT}, + Price: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://api.binance.com/api/v3/ticker/24hr?symbol=DCRBTC", + CurrencyPairDCRUSDT: "https://api.binance.com/api/v3/ticker/24hr?symbol=DCRUSDT", + }, + Depth: map[CurrencyPair]string{ + // Binance returns a maximum of 5000 depth chart points. This seems + // like it is the entire order book at least sometimes. + CurrencyPairDCRBTC: "https://api.binance.com/api/v3/depth?symbol=DCRBTC&limit=5000", + CurrencyPairDCRUSDT: "https://api.binance.com/api/v3/depth?symbol=DCRUSDT&limit=5000", + }, + Candlesticks: map[CurrencyPair]map[candlestickKey]string{ + CurrencyPairDCRBTC: { + hourKey: "https://api.binance.com/api/v3/klines?symbol=DCRBTC&interval=1h", + dayKey: "https://api.binance.com/api/v3/klines?symbol=DCRBTC&interval=1d", + monthKey: "https://api.binance.com/api/v3/klines?symbol=DCRBTC&interval=1M", + }, + CurrencyPairDCRUSDT: { + hourKey: "https://api.binance.com/api/v3/klines?symbol=DCRUSDT&interval=1h", + dayKey: "https://api.binance.com/api/v3/klines?symbol=DCRUSDT&interval=1d", + monthKey: "https://api.binance.com/api/v3/klines?symbol=DCRUSDT&interval=1M", + }, }, } BittrexURLs = URLs{ - Price: "https://api.bittrex.com/v3/markets/dcr-btc/ticker", - Stats: "https://api.bittrex.com/v3/markets/dcr-btc/summary", - Depth: "https://api.bittrex.com/v3/markets/dcr-btc/orderbook?depth=500", - Candlesticks: map[candlestickKey]string{ - hourKey: "https://api.bittrex.com/v3/markets/dcr-btc/candles/HOUR_1/recent", - dayKey: "https://api.bittrex.com/v3/markets/dcr-btc/candles/DAY_1/recent", + Markets: []CurrencyPair{CurrencyPairDCRBTC}, + Price: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://api.bittrex.com/v3/markets/dcr-btc/ticker", }, + Stats: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://api.bittrex.com/v3/markets/dcr-btc/summary", + }, + Depth: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://api.bittrex.com/v3/markets/dcr-btc/orderbook?depth=500", + }, + Candlesticks: map[CurrencyPair]map[candlestickKey]string{ + CurrencyPairDCRBTC: { + hourKey: "https://api.bittrex.com/v3/markets/dcr-btc/candles/HOUR_1/recent", + dayKey: "https://api.bittrex.com/v3/markets/dcr-btc/candles/DAY_1/recent", + }}, // Bittrex uses SignalR, which retrieves the actual websocket endpoint via // HTTP. Websocket: "socket.bittrex.com", } DragonExURLs = URLs{ - Price: "https://openapi.dragonex.io/api/v1/market/real/?symbol_id=1520101", + Markets: []CurrencyPair{CurrencyPairDCRBTC}, + Price: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://openapi.dragonex.io/api/v1/market/real/?symbol_id=1520101", + }, // DragonEx depth chart has no parameters for configuring amount of data. - Depth: "https://openapi.dragonex.io/api/v1/market/%s/?symbol_id=1520101", // Separate buy and sell endpoints - Candlesticks: map[candlestickKey]string{ - hourKey: "https://openapi.dragonex.io/api/v1/market/kline/?symbol_id=1520101&count=100&kline_type=5", - dayKey: "https://openapi.dragonex.io/api/v1/market/kline/?symbol_id=1520101&count=100&kline_type=6", + Depth: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://openapi.dragonex.io/api/v1/market/%s/?symbol_id=1520101", // Separate buy and sell endpoints + }, + Candlesticks: map[CurrencyPair]map[candlestickKey]string{ + CurrencyPairDCRBTC: { + hourKey: "https://openapi.dragonex.io/api/v1/market/kline/?symbol_id=1520101&count=100&kline_type=5", + dayKey: "https://openapi.dragonex.io/api/v1/market/kline/?symbol_id=1520101&count=100&kline_type=6", + }, }, } HuobiURLs = URLs{ - Price: "https://api.huobi.pro/market/detail/merged?symbol=dcrbtc", + Markets: []CurrencyPair{CurrencyPairDCRBTC}, + Price: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://api.huobi.pro/market/detail/merged?symbol=dcrbtc", + }, // Huobi's only depth parameter defines bin size, 'step0' seems to mean bin // width of zero. - Depth: "https://api.huobi.pro/market/depth?symbol=dcrbtc&type=step0", - Candlesticks: map[candlestickKey]string{ - hourKey: "https://api.huobi.pro/market/history/kline?symbol=dcrbtc&period=60min&size=2000", - dayKey: "https://api.huobi.pro/market/history/kline?symbol=dcrbtc&period=1day&size=2000", - monthKey: "https://api.huobi.pro/market/history/kline?symbol=dcrbtc&period=1mon&size=2000", + Depth: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://api.huobi.pro/market/depth?symbol=dcrbtc&type=step0", + }, + Candlesticks: map[CurrencyPair]map[candlestickKey]string{ + CurrencyPairDCRBTC: { + hourKey: "https://api.huobi.pro/market/history/kline?symbol=dcrbtc&period=60min&size=2000", + dayKey: "https://api.huobi.pro/market/history/kline?symbol=dcrbtc&period=1day&size=2000", + monthKey: "https://api.huobi.pro/market/history/kline?symbol=dcrbtc&period=1mon&size=2000", + }, }, } PoloniexURLs = URLs{ - Price: "https://poloniex.com/public?command=returnTicker", - // Maximum value of 100 for depth parameter. - Depth: "https://poloniex.com/public?command=returnOrderBook¤cyPair=BTC_DCR&depth=100", - Candlesticks: map[candlestickKey]string{ - halfHourKey: "https://poloniex.com/public?command=returnChartData¤cyPair=BTC_DCR&period=1800&start=0&resolution=auto", - dayKey: "https://poloniex.com/public?command=returnChartData¤cyPair=BTC_DCR&period=86400&start=0&resolution=auto", + Markets: []CurrencyPair{CurrencyPairDCRBTC}, + Price: map[CurrencyPair]string{ + CurrencyPairDCRBTC: "https://poloniex.com/public?command=returnTicker", + }, + Depth: map[CurrencyPair]string{ + // Maximum value of 100 for depth parameter. + CurrencyPairDCRBTC: "https://poloniex.com/public?command=returnOrderBook¤cyPair=BTC_DCR&depth=100", + }, + Candlesticks: map[CurrencyPair]map[candlestickKey]string{ + CurrencyPairDCRBTC: { + halfHourKey: "https://poloniex.com/public?command=returnChartData¤cyPair=BTC_DCR&period=1800&start=0&resolution=auto", + dayKey: "https://poloniex.com/public?command=returnChartData¤cyPair=BTC_DCR&period=86400&start=0&resolution=auto", + }, }, Websocket: "wss://api2.poloniex.com", } ) -// BtcIndices maps tokens to constructors for BTC-fiat exchanges. -var BtcIndices = map[string]func(*http.Client, *BotChannels) (Exchange, error){ +// Indices maps tokens to constructors for {BTC, USDT}-fiat exchanges. +var Indices = map[string]func(*http.Client, *BotChannels) (Exchange, error){ Coinbase: NewCoinbase, Coindesk: NewCoindesk, } -// DcrExchanges maps tokens to constructors for DCR-BTC exchanges. +// DcrExchanges maps tokens to constructors for DCR-{Asset} exchanges. var DcrExchanges = map[string]func(*http.Client, *BotChannels) (Exchange, error){ Binance: NewBinance, DragonEx: NewDragonEx, @@ -167,16 +275,17 @@ var DcrExchanges = map[string]func(*http.Client, *BotChannels) (Exchange, error) Cert: core.CertStore[dex.Mainnet]["dex.decred.org:7232"], CertHost: "dex.decred.org", }), + Mexc: NewMexc, } -// IsBtcIndex checks whether the given token is a known Bitcoin index, as -// opposed to a Decred-to-Bitcoin Exchange. -func IsBtcIndex(token string) bool { - _, ok := BtcIndices[token] +// IsIndex checks whether the given token is a known {Bitcoin, USDT} index, as +// opposed to a Decred-to-{Bitcoin, USDT} Exchange. +func IsIndex(token string) bool { + _, ok := Indices[token] return ok } -// IsDcrExchange checks whether the given token is a known Decred-BTC exchange. +// IsDcrExchange checks whether the given token is a known Decred-{Asset} exchange. func IsDcrExchange(token string) bool { _, ok := DcrExchanges[token] return ok @@ -184,9 +293,9 @@ func IsDcrExchange(token string) bool { // Tokens is a new slice of available exchange tokens. func Tokens() []string { - tokens := make([]string, 0, len(BtcIndices)+len(DcrExchanges)) + tokens := make([]string, 0, len(Indices)+len(DcrExchanges)) var token string - for token = range BtcIndices { + for token = range Indices { tokens = append(tokens, token) } for token = range DcrExchanges { @@ -274,8 +383,8 @@ func (sticks Candlesticks) needsUpdate(bin candlestickKey) bool { // BaseState. type BaseState struct { Price float64 `json:"price"` - // BaseVolume is poorly named. This is the volume in terms of (usually) BTC, - // not the base asset of any particular market. + // BaseVolume is poorly named. This is the volume in terms of (usually) BTC + // or USDT, not the base asset of any particular market. BaseVolume float64 `json:"base_volume,omitempty"` Volume float64 `json:"volume,omitempty"` Change float64 `json:"change,omitempty"` @@ -291,26 +400,6 @@ type ExchangeState struct { Candlesticks map[candlestickKey]Candlesticks `json:"candlesticks,omitempty"` } -/* -func (state *ExchangeState) copy() *ExchangeState { - newState := &ExchangeState{ - Price: state.Price, - BaseVolume: state.BaseVolume, - Volume: state.Volume, - Change: state.Change, - Stamp: state.Stamp, - Depth: state.Depth, - } - if state.Candlesticks != nil { - newState.Candlesticks = make(map[candlestickKey]Candlesticks) - for bin, sticks := range state.Candlesticks { - newState.Candlesticks[bin] = sticks - } - } - return newState -} -*/ - // Grab any candlesticks from the top that are not in the receiver. Candlesticks // are historical data, so never need to be discarded. func (state *ExchangeState) stealSticks(top *ExchangeState) { @@ -329,7 +418,7 @@ func (state *ExchangeState) stealSticks(top *ExchangeState) { } // Parse an ExchangeState from a protocol buffer message. -func exchangeStateFromProto(proto *dcrrates.ExchangeRateUpdate) *ExchangeState { +func exchangeStateFromProto(proto *dcrrates.ExchangeRateUpdate) (CurrencyPair, *ExchangeState) { state := &ExchangeState{ BaseState: BaseState{ Price: proto.GetPrice(), @@ -380,7 +469,8 @@ func exchangeStateFromProto(proto *dcrrates.ExchangeRateUpdate) *ExchangeState { } state.Candlesticks = stickMap } - return state + + return CurrencyPair(proto.CurrencyPair), state } // HasCandlesticks checks for data in the candlesticks map. @@ -405,6 +495,7 @@ func (state *ExchangeState) StickList() string { // ExchangeUpdate packages the ExchangeState for the update channel. type ExchangeUpdate struct { Token string + CurrencyPair State *ExchangeState } @@ -419,9 +510,9 @@ type Exchange interface { IsFailed() bool Token() string Hurry(time.Duration) - Update(*ExchangeState) - SilentUpdate(*ExchangeState) // skip passing update to the update channel - UpdateIndices(FiatIndices) + Update(CurrencyPair, *ExchangeState) + SilentUpdate(CurrencyPair, *ExchangeState) // skip passing update to the update channel + UpdateIndices(CurrencyPair, FiatIndices) } // Doer is an interface for a *http.Client to allow testing of Refresh paths. @@ -436,12 +527,12 @@ type CommonExchange struct { mtx sync.RWMutex token string URL string - currentState *ExchangeState + currentState map[CurrencyPair]*ExchangeState client Doer lastUpdate time.Time lastFail time.Time lastRequest time.Time - requests requests + requests map[CurrencyPair]*requests channels *BotChannels wsMtx sync.RWMutex ws websocketFeed @@ -457,7 +548,7 @@ type CommonExchange struct { wsProcessor WebsocketProcessor // Exchanges that use websockets or signalr to maintain a live orderbook can // use the buy and sell slices to leverage some useful methods on - // CommonExchange. + // CommonExchange. These fields are only for the BTC_DCR market. orderMtx sync.RWMutex buys wsOrders asks wsOrders @@ -525,39 +616,44 @@ func (xc *CommonExchange) fail(msg string, err error) { } // Update sends an updated ExchangeState to the ExchangeBot. -func (xc *CommonExchange) Update(state *ExchangeState) { - xc.update(state, true) +func (xc *CommonExchange) Update(market CurrencyPair, state *ExchangeState) { + xc.update(market, state, true) } // SilentUpdate stores the update for internal use, but does not signal an // update to the ExchangeBot. -func (xc *CommonExchange) SilentUpdate(state *ExchangeState) { - xc.update(state, false) +func (xc *CommonExchange) SilentUpdate(market CurrencyPair, state *ExchangeState) { + xc.update(market, state, false) } -func (xc *CommonExchange) update(state *ExchangeState, send bool) { +func (xc *CommonExchange) update(market CurrencyPair, state *ExchangeState, send bool) { xc.mtx.Lock() defer xc.mtx.Unlock() xc.lastUpdate = time.Now() - state.stealSticks(xc.currentState) - xc.currentState = state + currentState := xc.currentState[market] + if currentState != nil { + state.stealSticks(currentState) + } + xc.currentState[market] = state if !send { return } xc.channels.exchange <- &ExchangeUpdate{ - Token: xc.token, - State: state, + CurrencyPair: market, + Token: xc.token, + State: state, } } // UpdateIndices sends a bitcoin index update to the ExchangeBot. -func (xc *CommonExchange) UpdateIndices(indices FiatIndices) { +func (xc *CommonExchange) UpdateIndices(index CurrencyPair, indices FiatIndices) { xc.mtx.Lock() defer xc.mtx.Unlock() xc.lastUpdate = time.Now() xc.channels.index <- &IndexUpdate{ - Token: xc.token, - Indices: indices, + Token: xc.token, + CurrencyPair: index, + Indices: indices, } } @@ -575,11 +671,11 @@ func (xc *CommonExchange) fetch(request *http.Request, response interface{}) (er return } -// A thread-safe getter for the last known ExchangeState. -func (xc *CommonExchange) state() *ExchangeState { +// A thread-safe getter for the last known ExchangeState for supported markets. +func (xc *CommonExchange) state(market CurrencyPair) *ExchangeState { xc.mtx.RLock() defer xc.mtx.RUnlock() - return xc.currentState + return xc.currentState[market] } // WebsocketProcessor is a callback for new websocket messages from the server. @@ -820,13 +916,18 @@ func (xc *CommonExchange) wsDepthStatus(connector func()) (tryHttp, initializing // Used to initialize the embedding exchanges. func newCommonExchange(token string, client *http.Client, - reqs requests, channels *BotChannels) *CommonExchange { + reqs map[CurrencyPair]*requests, channels *BotChannels) *CommonExchange { + currentState := make(map[CurrencyPair]*ExchangeState, len(reqs)) + for mkt := range reqs { + currentState[mkt] = new(ExchangeState) + } + var tZero time.Time return &CommonExchange{ token: token, client: client, channels: channels, - currentState: new(ExchangeState), + currentState: currentState, lastUpdate: tZero, lastFail: tZero, lastRequest: tZero, @@ -843,10 +944,12 @@ type CoinbaseExchange struct { // NewCoinbase constructs a CoinbaseExchange. func NewCoinbase(client *http.Client, channels *BotChannels) (coinbase Exchange, err error) { - reqs := newRequests() - reqs.price, err = http.NewRequest(http.MethodGet, CoinbaseURLs.Price, nil) - if err != nil { - return + reqs := newRequests(CoinbaseURLs.Markets) + for mkt, price := range CoinbaseURLs.Price { + reqs[mkt].price, err = http.NewRequest(http.MethodGet, price, nil) + if err != nil { + return + } } coinbase = &CoinbaseExchange{ CommonExchange: newCommonExchange(Coinbase, client, reqs, channels), @@ -868,10 +971,16 @@ type CoinbaseResponseData struct { // Refresh retrieves and parses API data from Coinbase. func (coinbase *CoinbaseExchange) Refresh() { coinbase.LogRequest() + for mkt, reqs := range coinbase.requests { + coinbase.refresh(mkt, reqs) + } +} + +func (coinbase *CoinbaseExchange) refresh(mkt CurrencyPair, requests *requests) { response := new(CoinbaseResponse) - err := coinbase.fetch(coinbase.requests.price, response) + err := coinbase.fetch(requests.price, response) if err != nil { - coinbase.fail("Fetch", err) + coinbase.fail(fmt.Sprintf("%s: Fetch", mkt), err) return } @@ -879,26 +988,29 @@ func (coinbase *CoinbaseExchange) Refresh() { for code, floatStr := range response.Data.Rates { price, err := strconv.ParseFloat(floatStr, 64) if err != nil { - coinbase.fail(fmt.Sprintf("Failed to parse float for index %s. Given %s", code, floatStr), err) + coinbase.fail(fmt.Sprintf("%s: Failed to parse float for index %s. Given %s", mkt, code, floatStr), err) continue } indices[code] = price } - coinbase.UpdateIndices(indices) + coinbase.UpdateIndices(mkt, indices) } -// CoindeskExchange provides Bitcoin indices for USD, GBP, and EUR by default. -// Others are available, but custom requests would need to be implemented. +// CoindeskExchange provides {Bitcoin, USDT} indices for USD, GBP, and EUR by +// default. Others are available, but custom requests would need to be +// implemented. type CoindeskExchange struct { *CommonExchange } // NewCoindesk constructs a CoindeskExchange. func NewCoindesk(client *http.Client, channels *BotChannels) (coindesk Exchange, err error) { - reqs := newRequests() - reqs.price, err = http.NewRequest(http.MethodGet, CoindeskURLs.Price, nil) - if err != nil { - return + reqs := newRequests(CoindeskURLs.Markets) + for index, price := range CoindeskURLs.Price { + reqs[index].price, err = http.NewRequest(http.MethodGet, price, nil) + if err != nil { + return + } } coindesk = &CoindeskExchange{ CommonExchange: newCommonExchange(Coindesk, client, reqs, channels), @@ -933,8 +1045,14 @@ type CoindeskResponseBpi struct { // Refresh retrieves and parses API data from Coindesk. func (coindesk *CoindeskExchange) Refresh() { coindesk.LogRequest() + for index, requests := range coindesk.requests { + coindesk.refresh(index, requests) + } +} + +func (coindesk *CoindeskExchange) refresh(index CurrencyPair, requests *requests) { response := new(CoindeskResponse) - err := coindesk.fetch(coindesk.requests.price, response) + err := coindesk.fetch(requests.price, response) if err != nil { coindesk.fail("Fetch", err) return @@ -944,7 +1062,7 @@ func (coindesk *CoindeskExchange) Refresh() { for code, bpi := range response.Bpi { indices[code] = bpi.RateFloat } - coindesk.UpdateIndices(indices) + coindesk.UpdateIndices(index, indices) } // BinanceExchange is a high-volume and well-respected crypto exchange. @@ -954,23 +1072,30 @@ type BinanceExchange struct { // NewBinance constructs a BinanceExchange. func NewBinance(client *http.Client, channels *BotChannels) (binance Exchange, err error) { - reqs := newRequests() - reqs.price, err = http.NewRequest(http.MethodGet, BinanceURLs.Price, nil) - if err != nil { - return - } - - reqs.depth, err = http.NewRequest(http.MethodGet, BinanceURLs.Depth, nil) - if err != nil { - return + reqs := newRequests(BinanceURLs.Markets) + for mkt, price := range BinanceURLs.Price { + reqs[mkt].price, err = http.NewRequest(http.MethodGet, price, nil) + if err != nil { + return + } } - for dur, url := range BinanceURLs.Candlesticks { - reqs.candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + for mkt, depth := range BinanceURLs.Depth { + reqs[mkt].depth, err = http.NewRequest(http.MethodGet, depth, nil) if err != nil { return } } + + for mkt, candlesticks := range BinanceURLs.Candlesticks { + for dur, url := range candlesticks { + reqs[mkt].candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return + } + } + } + binance = &BinanceExchange{ CommonExchange: newCommonExchange(Binance, client, reqs, channels), } @@ -1002,10 +1127,9 @@ type BinancePriceResponse struct { Count int64 `json:"count"` } -// BinanceCandlestickResponse models candlestick data returned from the Binance -// API. Binance has a response with mixed-type arrays, so type-checking is -// appropriate. Sample response is -// [ +// CandlestickResponse models candlestick data returned from the Mexc and Binance +// API. The candlestick response has mixed-type arrays, so type-checking is +// appropriate. Sample response is [ // // [ // 1499040000000, // Open time @@ -1014,73 +1138,74 @@ type BinancePriceResponse struct { // "0.01575800", // Low // "0.01577100", // Close // "148976.11427815", // Volume -// ... +// 1640804940000, // Close Time (Mexc Only) +// "168387.3" // Quote Asset Volume (Mexc Only) // ] // // ] -type BinanceCandlestickResponse [][]interface{} +type CandlestickResponse [][]interface{} -func badBinanceStickElement(key string, element interface{}) Candlesticks { - log.Errorf("Unable to decode %s from Binance candlestick: %T: %v", key, element, element) +func badStickElement(key string, element interface{}) Candlesticks { + log.Errorf("Unable to decode %s from candlestick: %T: %v", key, element, element) return Candlesticks{} } -func (r BinanceCandlestickResponse) translate() Candlesticks { +func (r CandlestickResponse) translate() Candlesticks { sticks := make(Candlesticks, 0, len(r)) for _, rawStick := range r { if len(rawStick) < 6 { - log.Error("Unable to decode Binance candlestick response. Not enough elements.") + log.Error("Unable to decode candlestick response. Not enough elements.") return Candlesticks{} } unixMsFlt, ok := rawStick[0].(float64) if !ok { - return badBinanceStickElement("start time", rawStick[0]) + return badStickElement("start time", rawStick[0]) } startTime := time.Unix(int64(unixMsFlt/1e3), 0) openStr, ok := rawStick[1].(string) if !ok { - return badBinanceStickElement("open", rawStick[1]) + return badStickElement("open", rawStick[1]) } open, err := strconv.ParseFloat(openStr, 64) if err != nil { - return badBinanceStickElement("open float", err) + return badStickElement("open float", err) } highStr, ok := rawStick[2].(string) if !ok { - return badBinanceStickElement("high", rawStick[2]) + return badStickElement("high", rawStick[2]) } high, err := strconv.ParseFloat(highStr, 64) if err != nil { - return badBinanceStickElement("high float", err) + return badStickElement("high float", err) } lowStr, ok := rawStick[3].(string) if !ok { - return badBinanceStickElement("low", rawStick[3]) + return badStickElement("low", rawStick[3]) } low, err := strconv.ParseFloat(lowStr, 64) if err != nil { - return badBinanceStickElement("low float", err) + return badStickElement("low float", err) } closeStr, ok := rawStick[4].(string) if !ok { - return badBinanceStickElement("close", rawStick[4]) + return badStickElement("close", rawStick[4]) } close, err := strconv.ParseFloat(closeStr, 64) if err != nil { - return badBinanceStickElement("close float", err) + return badStickElement("close float", err) } volumeStr, ok := rawStick[5].(string) if !ok { - return badBinanceStickElement("volume", rawStick[5]) + return badStickElement("volume", rawStick[5]) } volume, err := strconv.ParseFloat(volumeStr, 64) if err != nil { - return badBinanceStickElement("volume float", err) + return badStickElement("volume float", err) } sticks = append(sticks, Candlestick{ @@ -1102,17 +1227,17 @@ type BinanceDepthResponse struct { Asks [][2]string } -func parseBinanceDepthPoints(pts [][2]string) ([]DepthPoint, error) { +func parseDepthPoints(pts [][2]string) ([]DepthPoint, error) { outPts := make([]DepthPoint, 0, len(pts)) for _, pt := range pts { price, err := strconv.ParseFloat(pt[0], 64) if err != nil { - return outPts, fmt.Errorf("Unable to parse Binance depth point price: %v", err) + return outPts, fmt.Errorf("Unable to parse depth point price: %v", err) } quantity, err := strconv.ParseFloat(pt[1], 64) if err != nil { - return outPts, fmt.Errorf("Unable to parse Binance depth point quantity: %v", err) + return outPts, fmt.Errorf("Unable to parse depth point quantity: %v", err) } outPts = append(outPts, DepthPoint{ @@ -1123,21 +1248,18 @@ func parseBinanceDepthPoints(pts [][2]string) ([]DepthPoint, error) { return outPts, nil } -func (r *BinanceDepthResponse) translate() *DepthData { - if r == nil { - return nil - } +func translateDepthPoints(xc string, asks [][2]string, bids [][2]string) *DepthData { depth := new(DepthData) depth.Time = time.Now().Unix() var err error - depth.Asks, err = parseBinanceDepthPoints(r.Asks) + depth.Asks, err = parseDepthPoints(asks) if err != nil { - log.Errorf("%v", err) + log.Errorf("%s: %v", xc, err) return nil } - depth.Bids, err = parseBinanceDepthPoints(r.Bids) + depth.Bids, err = parseDepthPoints(bids) if err != nil { - log.Errorf("%v", err) + log.Errorf("%s: %v", xc, err) return nil } return depth @@ -1146,51 +1268,57 @@ func (r *BinanceDepthResponse) translate() *DepthData { // Refresh retrieves and parses API data from Binance. func (binance *BinanceExchange) Refresh() { binance.LogRequest() + for mkt, requests := range binance.requests { + binance.refresh(mkt, requests) + } +} + +func (binance *BinanceExchange) refresh(mkt CurrencyPair, requests *requests) { priceResponse := new(BinancePriceResponse) - err := binance.fetch(binance.requests.price, priceResponse) + err := binance.fetch(requests.price, priceResponse) if err != nil { - binance.fail("Fetch price", err) + binance.fail(fmt.Sprintf("%s: Fetch price", mkt), err) return } price, err := strconv.ParseFloat(priceResponse.LastPrice, 64) if err != nil { - binance.fail(fmt.Sprintf("Failed to parse float from LastPrice=%s", priceResponse.LastPrice), err) + binance.fail(fmt.Sprintf("%s: Failed to parse float from LastPrice=%s", mkt, priceResponse.LastPrice), err) return } baseVolume, err := strconv.ParseFloat(priceResponse.QuoteVolume, 64) if err != nil { - binance.fail(fmt.Sprintf("Failed to parse float from QuoteVolume=%s", priceResponse.QuoteVolume), err) + binance.fail(fmt.Sprintf("%s: Failed to parse float from QuoteVolume=%s", mkt, priceResponse.QuoteVolume), err) return } dcrVolume, err := strconv.ParseFloat(priceResponse.Volume, 64) if err != nil { - binance.fail(fmt.Sprintf("Failed to parse float from Volume=%s", priceResponse.Volume), err) + binance.fail(fmt.Sprintf("%s: Failed to parse float from Volume=%s", mkt, priceResponse.Volume), err) return } priceChange, err := strconv.ParseFloat(priceResponse.PriceChange, 64) if err != nil { - binance.fail(fmt.Sprintf("Failed to parse float from PriceChange=%s", priceResponse.PriceChange), err) + binance.fail(fmt.Sprintf("%s: Failed to parse float from PriceChange=%s", mkt, priceResponse.PriceChange), err) return } // Get the depth chart depthResponse := new(BinanceDepthResponse) - err = binance.fetch(binance.requests.depth, depthResponse) + err = binance.fetch(requests.depth, depthResponse) if err != nil { - log.Errorf("Error retrieving depth chart data from Binance: %v", err) + log.Errorf("Error retrieving depth chart data from Binance(%s): %v", mkt, err) } - depth := depthResponse.translate() + depth := translateDepthPoints(Binance, depthResponse.Asks, depthResponse.Bids) // Grab the current state to check if candlesticks need updating - state := binance.state() + state := binance.state(mkt) candlesticks := map[candlestickKey]Candlesticks{} - for bin, req := range binance.requests.candlesticks { + for bin, req := range requests.candlesticks { oldSticks, found := state.Candlesticks[bin] if !found || oldSticks.needsUpdate(bin) { - log.Tracef("Signalling candlestick update for %s, bin size %s", binance.token, bin) - response := new(BinanceCandlestickResponse) + log.Tracef("Signalling candlestick update for %s, market %s, bin size %s", binance.token, mkt, bin) + response := new(CandlestickResponse) err := binance.fetch(req, response) if err != nil { log.Errorf("Error retrieving candlestick data from binance for bin size %s: %v", string(bin), err) @@ -1204,7 +1332,7 @@ func (binance *BinanceExchange) Refresh() { } } - binance.Update(&ExchangeState{ + binance.Update(mkt, &ExchangeState{ BaseState: BaseState{ Price: price, BaseVolume: baseVolume, @@ -1221,43 +1349,54 @@ func (binance *BinanceExchange) Refresh() { type DragonExchange struct { *CommonExchange SymbolID int - depthBuyRequest *http.Request - depthSellRequest *http.Request + depthBuyRequest map[CurrencyPair]*http.Request + depthSellRequest map[CurrencyPair]*http.Request } // NewDragonEx constructs a DragonExchange. func NewDragonEx(client *http.Client, channels *BotChannels) (dragonex Exchange, err error) { - reqs := newRequests() - reqs.price, err = http.NewRequest(http.MethodGet, DragonExURLs.Price, nil) - if err != nil { - return - } - - // Dragonex has separate endpoints for buy and sell, so the requests are - // stored as fields of DragonExchange - var depthSell, depthBuy *http.Request - depthSell, err = http.NewRequest(http.MethodGet, fmt.Sprintf(DragonExURLs.Depth, "sell"), nil) - if err != nil { - return + reqs := newRequests(DragonExURLs.Markets) + for mkt, price := range DragonExURLs.Price { + reqs[mkt].price, err = http.NewRequest(http.MethodGet, price, nil) + if err != nil { + return + } } - depthBuy, err = http.NewRequest(http.MethodGet, fmt.Sprintf(DragonExURLs.Depth, "buy"), nil) - if err != nil { - return - } + depthBuyMap := make(map[CurrencyPair]*http.Request, len(reqs)) + depthSellMap := make(map[CurrencyPair]*http.Request, len(reqs)) + for mkt, depth := range DragonExURLs.Depth { + // Dragonex has separate endpoints for buy and sell, so the requests are + // stored as fields of DragonExchange + var depthSell, depthBuy *http.Request + depthSell, err = http.NewRequest(http.MethodGet, fmt.Sprintf(depth, "sell"), nil) + if err != nil { + return + } - for dur, url := range DragonExURLs.Candlesticks { - reqs.candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + depthBuy, err = http.NewRequest(http.MethodGet, fmt.Sprintf(depth, "buy"), nil) if err != nil { return } + + depthBuyMap[mkt] = depthBuy + depthSellMap[mkt] = depthSell + } + + for mkt, candlesticks := range DragonExURLs.Candlesticks { + for dur, url := range candlesticks { + reqs[mkt].candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return + } + } } dragonex = &DragonExchange{ CommonExchange: newCommonExchange(DragonEx, client, reqs, channels), SymbolID: 1520101, - depthBuyRequest: depthBuy, - depthSellRequest: depthSell, + depthBuyRequest: depthBuyMap, + depthSellRequest: depthSellMap, } return } @@ -1482,53 +1621,59 @@ func (dragonex *DragonExchange) getDragonExDepthData(req *http.Request, response // Refresh retrieves and parses API data from DragonEx. func (dragonex *DragonExchange) Refresh() { dragonex.LogRequest() + for mkt, req := range dragonex.requests { + dragonex.refresh(mkt, req) + } +} + +func (dragonex *DragonExchange) refresh(mkt CurrencyPair, requests *requests) { response := new(DragonExPriceResponse) - err := dragonex.fetch(dragonex.requests.price, response) + err := dragonex.fetch(requests.price, response) if err != nil { - dragonex.fail("Fetch", err) + dragonex.fail(fmt.Sprintf("%s: Fetch", mkt), err) return } if !response.Ok { - dragonex.fail("Response not ok", err) + dragonex.fail(fmt.Sprintf("%s: Response not ok", mkt), err) return } if len(response.Data) == 0 { - dragonex.fail("No data", fmt.Errorf("Response data array is empty")) + dragonex.fail(fmt.Sprintf("%s: No data", mkt), fmt.Errorf("Response data array is empty")) return } data := response.Data[0] if data.SymbolID != dragonex.SymbolID { - dragonex.fail("Wrong code", fmt.Errorf("Pair id %d in response is not the expected id %d", data.SymbolID, dragonex.SymbolID)) + dragonex.fail(fmt.Sprintf("%s: Wrong code", mkt), fmt.Errorf("Pair id %d in response is not the expected id %d", data.SymbolID, dragonex.SymbolID)) return } price, err := strconv.ParseFloat(data.ClosePrice, 64) if err != nil { - dragonex.fail(fmt.Sprintf("Failed to parse float from ClosePrice=%s", data.ClosePrice), err) + dragonex.fail(fmt.Sprintf("%s: Failed to parse float from ClosePrice=%s", mkt, data.ClosePrice), err) return } volume, err := strconv.ParseFloat(data.TotalVolume, 64) if err != nil { - dragonex.fail(fmt.Sprintf("Failed to parse float from TotalVolume=%s", data.TotalVolume), err) + dragonex.fail(fmt.Sprintf("%s: Failed to parse float from TotalVolume=%s", mkt, data.TotalVolume), err) return } btcVolume := volume * price priceChange, err := strconv.ParseFloat(data.PriceChange, 64) if err != nil { - dragonex.fail(fmt.Sprintf("Failed to parse float from PriceChange=%s", data.PriceChange), err) + dragonex.fail(fmt.Sprintf("%s: Failed to parse float from PriceChange=%s", mkt, data.PriceChange), err) return } // Depth chart depthSellResponse := new(DragonExDepthResponse) - sellErr := dragonex.getDragonExDepthData(dragonex.depthSellRequest, depthSellResponse) + sellErr := dragonex.getDragonExDepthData(dragonex.depthSellRequest[mkt], depthSellResponse) if sellErr != nil { - log.Errorf("DragonEx sell order book response error: %v", sellErr) + log.Errorf("%s: DragonEx sell order book response error: %v", mkt, sellErr) } depthBuyResponse := new(DragonExDepthResponse) - buyErr := dragonex.getDragonExDepthData(dragonex.depthBuyRequest, depthBuyResponse) + buyErr := dragonex.getDragonExDepthData(dragonex.depthBuyRequest[mkt], depthBuyResponse) if buyErr != nil { - log.Errorf("DragonEx buy order book response error: %v", buyErr) + log.Errorf("%s: DragonEx buy order book response error: %v", mkt, buyErr) } var depth *DepthData @@ -1541,10 +1686,10 @@ func (dragonex *DragonExchange) Refresh() { } // Grab the current state to check if candlesticks need updating - state := dragonex.state() + state := dragonex.state(mkt) candlesticks := map[candlestickKey]Candlesticks{} - for bin, req := range dragonex.requests.candlesticks { + for bin, req := range requests.candlesticks { oldSticks, found := state.Candlesticks[bin] if !found || oldSticks.needsUpdate(bin) { log.Tracef("Signalling candlestick update for %s, bin size %s", dragonex.token, bin) @@ -1565,7 +1710,7 @@ func (dragonex *DragonExchange) Refresh() { } } - dragonex.Update(&ExchangeState{ + dragonex.Update(mkt, &ExchangeState{ BaseState: BaseState{ Price: price, BaseVolume: btcVolume, @@ -1586,25 +1731,33 @@ type HuobiExchange struct { // NewHuobi constructs a HuobiExchange. func NewHuobi(client *http.Client, channels *BotChannels) (huobi Exchange, err error) { - reqs := newRequests() - reqs.price, err = http.NewRequest(http.MethodGet, HuobiURLs.Price, nil) - if err != nil { - return - } - reqs.price.Header.Add("Content-Type", "application/x-www-form-urlencoded") + reqs := newRequests(HuobiURLs.Markets) + for mkt, price := range HuobiURLs.Price { + reqs[mkt].price, err = http.NewRequest(http.MethodGet, price, nil) + if err != nil { + return + } - reqs.depth, err = http.NewRequest(http.MethodGet, HuobiURLs.Depth, nil) - if err != nil { - return + reqs[mkt].price.Header.Add("Content-Type", "application/x-www-form-urlencoded") } - reqs.depth.Header.Add("Content-Type", "application/x-www-form-urlencoded") - for dur, url := range HuobiURLs.Candlesticks { - reqs.candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + for mkt, depth := range HuobiURLs.Depth { + reqs[mkt].depth, err = http.NewRequest(http.MethodGet, depth, nil) if err != nil { return } - reqs.candlesticks[dur].Header.Add("Content-Type", "application/x-www-form-urlencoded") + + reqs[mkt].depth.Header.Add("Content-Type", "application/x-www-form-urlencoded") + } + + for mkt, candlesticks := range HuobiURLs.Candlesticks { + for dur, url := range candlesticks { + reqs[mkt].candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return + } + reqs[mkt].candlesticks[dur].Header.Add("Content-Type", "application/x-www-form-urlencoded") + } } return &HuobiExchange{ @@ -1711,14 +1864,20 @@ type HuobiCandlestickResponse struct { // Refresh retrieves and parses API data from Huobi. func (huobi *HuobiExchange) Refresh() { huobi.LogRequest() + for mkt, requests := range huobi.requests { + huobi.refresh(mkt, requests) + } +} + +func (huobi *HuobiExchange) refresh(mkt CurrencyPair, requests *requests) { priceResponse := new(HuobiPriceResponse) - err := huobi.fetch(huobi.requests.price, priceResponse) + err := huobi.fetch(requests.price, priceResponse) if err != nil { - huobi.fail("Fetch", err) + huobi.fail(fmt.Sprintf("%s: Fetch", mkt), err) return } if priceResponse.Status != huobi.Ok { - huobi.fail("Status not ok", fmt.Errorf("Expected status %s. Received %s", huobi.Ok, priceResponse.Status)) + huobi.fail("Status not ok", fmt.Errorf("%s: Expected status %s. Received %s", mkt, huobi.Ok, priceResponse.Status)) return } baseVolume := priceResponse.Tick.Vol @@ -1726,11 +1885,11 @@ func (huobi *HuobiExchange) Refresh() { // Depth data var depth *DepthData depthResponse := new(HuobiDepthResponse) - err = huobi.fetch(huobi.requests.depth, depthResponse) + err = huobi.fetch(requests.depth, depthResponse) if err != nil { - log.Errorf("Huobi depth chart fetch error: %v", err) + log.Errorf("%s: Huobi depth chart fetch error: %v", mkt, err) } else if depthResponse.Status != huobi.Ok { - log.Errorf("Huobi server depth response error. status: %s", depthResponse.Status) + log.Errorf("%s: Huobi server depth response error. status: %s", mkt, depthResponse.Status) } else { depth = &DepthData{ Time: depthResponse.Ts / 1000, @@ -1740,20 +1899,20 @@ func (huobi *HuobiExchange) Refresh() { } // Candlestick data - state := huobi.state() + state := huobi.state(mkt) candlesticks := map[candlestickKey]Candlesticks{} - for bin, req := range huobi.requests.candlesticks { + for bin, req := range requests.candlesticks { oldSticks, found := state.Candlesticks[bin] if !found || oldSticks.needsUpdate(bin) { - log.Tracef("Signalling candlestick update for %s, bin size %s", huobi.token, bin) + log.Tracef("%s: Signalling candlestick update for %s, bin size %s", mkt, huobi.token, bin) response := new(HuobiCandlestickResponse) err := huobi.fetch(req, response) if err != nil { - log.Errorf("Error retrieving candlestick data from huobi for bin size %s: %v", string(bin), err) + log.Errorf("%s: Error retrieving candlestick data from huobi for bin size %s: %v", mkt, string(bin), err) continue } if response.Status != huobi.Ok { - log.Errorf("Huobi server error while fetching candlestick data. status: %s", response.Status) + log.Errorf("%s: Huobi server error while fetching candlestick data. status: %s", mkt, response.Status) continue } @@ -1764,7 +1923,7 @@ func (huobi *HuobiExchange) Refresh() { } } - huobi.Update(&ExchangeState{ + huobi.Update(mkt, &ExchangeState{ BaseState: BaseState{ Price: priceResponse.Tick.Close, BaseVolume: baseVolume, @@ -1780,33 +1939,47 @@ func (huobi *HuobiExchange) Refresh() { // PoloniexExchange is a U.S.-based exchange. type PoloniexExchange struct { *CommonExchange - CurrencyPair string - orderSeq int64 + markets []string + orderSeq int64 } // NewPoloniex constructs a PoloniexExchange. func NewPoloniex(client *http.Client, channels *BotChannels) (poloniex Exchange, err error) { - reqs := newRequests() - reqs.price, err = http.NewRequest(http.MethodGet, PoloniexURLs.Price, nil) - if err != nil { - return - } + reqs := newRequests(PoloniexURLs.Markets) + var markets []string + for mkt, price := range PoloniexURLs.Price { + reqs[mkt].price, err = http.NewRequest(http.MethodGet, price, nil) + if err != nil { + return + } - reqs.depth, err = http.NewRequest(http.MethodGet, PoloniexURLs.Depth, nil) - if err != nil { - return + switch mkt { + case CurrencyPairDCRBTC: + markets = append(markets, "BTC_DCR") + case CurrencyPairDCRUSDT: + markets = append(markets, "DCR_USDT") + } } - for dur, url := range PoloniexURLs.Candlesticks { - reqs.candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + for mkt, depth := range PoloniexURLs.Depth { + reqs[mkt].depth, err = http.NewRequest(http.MethodGet, depth, nil) if err != nil { return } } + for mkt, candlesticks := range PoloniexURLs.Candlesticks { + for dur, url := range candlesticks { + reqs[mkt].candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return + } + } + } + p := &PoloniexExchange{ CommonExchange: newCommonExchange(Poloniex, client, reqs, channels), - CurrencyPair: "BTC_DCR", + markets: markets, } go func() { <-channels.done @@ -1911,8 +2084,6 @@ func (r *PoloniexDepthResponse) translate() *DepthData { } // PoloniexCandlestickResponse models the k-line data response from Poloniex. -// {"date":1463356800,"high":1,"low":0.0037,"open":1,"close":0.00432007,"volume":357.23057396,"quoteVolume":76195.11422729,"weightedAverage":0.00468836} - type PoloniexCandlestickPt struct { Date int64 `json:"date"` High float64 `json:"high"` @@ -1949,7 +2120,7 @@ type poloniexWsSubscription struct { var poloniexOrderbookSubscription = poloniexWsSubscription{ Command: "subscribe", - Channel: 162, + Channel: 162, // BTC_DCR, No orderbook support for other dcr pairs. } // The final structure to parse in the initial websocket message is a map of the @@ -2165,7 +2336,7 @@ func (poloniex *PoloniexExchange) processWsMessage(raw []byte) { } switch len(msg) { case 1: - // Likely a heatbeat + // Likely a heartbeat code, ok := msg[0].(float64) if !ok { poloniex.setWsFail(fmt.Errorf("non-integer single-element poloniex response of implicit type %T", msg[0])) @@ -2199,10 +2370,10 @@ func (poloniex *PoloniexExchange) processWsMessage(raw []byte) { if code == poloniexInitialOrderbookKey { poloniex.processWsOrderbook(seq, responseList) - state := poloniex.state() + state := poloniex.state(CurrencyPairDCRBTC) if state != nil { // Only send update if price has been fetched depth := poloniex.wsDepths() - poloniex.Update(&ExchangeState{ + poloniex.Update(CurrencyPairDCRBTC, &ExchangeState{ BaseState: BaseState{ Price: state.Price, BaseVolume: state.BaseVolume, @@ -2293,14 +2464,14 @@ func (poloniex *PoloniexExchange) Refresh() { poloniex.LogRequest() var response map[string]*PoloniexPair - err := poloniex.fetch(poloniex.requests.price, &response) + err := poloniex.fetch(poloniex.requests[CurrencyPairDCRBTC].price, &response) if err != nil { poloniex.fail("Fetch", err) return } - market, ok := response[poloniex.CurrencyPair] + market, ok := response[poloniex.markets[0]] if !ok { - poloniex.fail("Market not in response", fmt.Errorf("Response did not have expected CurrencyPair %s", poloniex.CurrencyPair)) + poloniex.fail("Market not in response", fmt.Errorf("Response did not have expected CurrencyPair %s", poloniex.markets[0])) return } price, err := strconv.ParseFloat(market.Last, 64) @@ -2331,7 +2502,7 @@ func (poloniex *PoloniexExchange) Refresh() { // If not expecting depth data from the websocket, grab it from HTTP if tryHttp { depthResponse := new(PoloniexDepthResponse) - err = poloniex.fetch(poloniex.requests.depth, depthResponse) + err = poloniex.fetch(poloniex.requests[CurrencyPairDCRBTC].depth, depthResponse) if err != nil { log.Errorf("Poloniex depth chart fetch error: %v", err) } @@ -2347,10 +2518,10 @@ func (poloniex *PoloniexExchange) Refresh() { } // Candlesticks - state := poloniex.state() + state := poloniex.state(CurrencyPairDCRBTC) candlesticks := map[candlestickKey]Candlesticks{} - for bin, req := range poloniex.requests.candlesticks { + for bin, req := range poloniex.requests[CurrencyPairDCRBTC].candlesticks { oldSticks, found := state.Candlesticks[bin] if !found || oldSticks.needsUpdate(bin) { log.Tracef("Signalling candlestick update for %s, bin size %s", poloniex.token, bin) @@ -2379,9 +2550,9 @@ func (poloniex *PoloniexExchange) Refresh() { Candlesticks: candlesticks, } if wsStarting { - poloniex.SilentUpdate(update) + poloniex.SilentUpdate(CurrencyPairDCRBTC, update) } else { - poloniex.Update(update) + poloniex.Update(CurrencyPairDCRBTC, update) } } @@ -2430,7 +2601,7 @@ type DecredDEX struct { func NewDecredDEXConstructor(cfg *DEXConfig) func(*http.Client, *BotChannels) (Exchange, error) { return func(client *http.Client, channels *BotChannels) (Exchange, error) { dcr := &DecredDEX{ - CommonExchange: newCommonExchange(cfg.Token, client, requests{}, channels), + CommonExchange: newCommonExchange(cfg.Token, client, make(map[CurrencyPair]*requests), channels), candleCaches: make(map[uint64]*candleCache), reqs: make(map[uint64]func(*msgjson.Message)), cfg: cfg, @@ -2493,7 +2664,7 @@ func (dcr *DecredDEX) Refresh() { return // no rate, nothing to do. } - dcr.Update(&ExchangeState{ + dcr.Update(CurrencyPairDCRBTC, &ExchangeState{ BaseState: BaseState{ Price: dcr.lastRate, Change: change, @@ -2866,7 +3037,7 @@ func (dcr *DecredDEX) setOrderBook(ob *msgjson.OrderBook) { dcr.lastRate = depth.MidGap() } - dcr.Update(&ExchangeState{ + dcr.Update(CurrencyPairDCRBTC, &ExchangeState{ BaseState: BaseState{ Price: dcr.lastRate, // Change: priceChange, // With candlesticks @@ -2942,3 +3113,147 @@ func (dcr *DecredDEX) updateRemaining(update *msgjson.UpdateRemainingNote) { delete(side, rateKey) } } + +// MexcExchange is a high-volume and well-respected crypto exchange. +type MexcExchange struct { + *CommonExchange +} + +// NewMexc constructs a *MexcExchange. +func NewMexc(client *http.Client, channels *BotChannels) (mexc Exchange, err error) { + reqs := newRequests(MexcURLs.Markets) + for mkt, price := range MexcURLs.Price { + reqs[mkt].price, err = http.NewRequest(http.MethodGet, price, nil) + if err != nil { + return + } + } + + for mkt, depth := range MexcURLs.Depth { + reqs[mkt].depth, err = http.NewRequest(http.MethodGet, depth, nil) + if err != nil { + return + } + } + + for mkt, candlesticks := range MexcURLs.Candlesticks { + for dur, url := range candlesticks { + reqs[mkt].candlesticks[dur], err = http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return + } + } + } + + mexc = &MexcExchange{ + CommonExchange: newCommonExchange(Mexc, client, reqs, channels), + } + return +} + +// MexcPriceResponse models the JSON price data returned from the Mexc API. +type MexcPriceResponse struct { + Symbol string `json:"symbol"` + PriceChange string `json:"priceChange"` + PriceChangePercent string `json:"priceChangePercent"` + PrevClosePrice string `json:"prevClosePrice"` + LastPrice string `json:"lastPrice"` + BidPrice string `json:"bidPrice"` + BidQty string `json:"bidQty"` + AskPrice string `json:"askPrice"` + AskQty string `json:"askQty"` + OpenPrice string `json:"openPrice"` + HighPrice string `json:"highPrice"` + LowPrice string `json:"lowPrice"` + Volume string `json:"volume"` + QuoteVolume string `json:"quoteVolume"` + OpenTime int64 `json:"openTime"` + CloseTime int64 `json:"closeTime"` +} + +// MexcDepthResponse models the response for Mexc depth chart data. +type MexcDepthResponse struct { + UpdateID int64 `json:"lastUpdateId"` + Bids [][2]string + Asks [][2]string +} + +// Refresh retrieves and parses API data from Mexc Exchange. +func (mexc *MexcExchange) Refresh() { + mexc.LogRequest() + for currencyPair, requests := range mexc.requests { + mexc.refresh(currencyPair, requests) + } +} + +func (mexc *MexcExchange) refresh(pair CurrencyPair, requests *requests) { + priceResponse := new(MexcPriceResponse) + err := mexc.fetch(requests.price, priceResponse) + if err != nil { + mexc.fail(fmt.Sprintf("%s: Fetch price", pair), err) + return + } + price, err := strconv.ParseFloat(priceResponse.LastPrice, 64) + if err != nil { + mexc.fail(fmt.Sprintf("%s: Failed to parse float from LastPrice=%s", pair, priceResponse.LastPrice), err) + return + } + baseVolume, err := strconv.ParseFloat(priceResponse.QuoteVolume, 64) + if err != nil { + mexc.fail(fmt.Sprintf("%s: Failed to parse float from QuoteVolume=%s", pair, priceResponse.QuoteVolume), err) + return + } + + dcrVolume, err := strconv.ParseFloat(priceResponse.Volume, 64) + if err != nil { + mexc.fail(fmt.Sprintf("%s: Failed to parse float from Volume=%s", pair, priceResponse.Volume), err) + return + } + priceChange, err := strconv.ParseFloat(priceResponse.PriceChange, 64) + if err != nil { + mexc.fail(fmt.Sprintf("%s: Failed to parse float from PriceChange=%s", pair, priceResponse.PriceChange), err) + return + } + + // Get the depth chart + depthResponse := new(MexcDepthResponse) + err = mexc.fetch(requests.depth, depthResponse) + if err != nil { + log.Errorf("Error retrieving depth chart data from Mexc(%s): %v", pair, err) + } + depth := translateDepthPoints(Mexc, depthResponse.Asks, depthResponse.Bids) + + // Grab the current state to check if candlesticks need updating + state := mexc.state(pair) + + candlesticks := map[candlestickKey]Candlesticks{} + for bin, req := range requests.candlesticks { + oldSticks, found := state.Candlesticks[bin] + if !found || oldSticks.needsUpdate(bin) { + log.Tracef("Signalling candlestick update for %s, market %s, bin size %s", mexc.token, pair, bin) + response := new(CandlestickResponse) + err := mexc.fetch(req, response) + if err != nil { + log.Errorf("Error retrieving candlestick data from mexc for bin size %s: %v", string(bin), err) + continue + } + sticks := response.translate() + + if !found || sticks.time().After(oldSticks.time()) { + candlesticks[bin] = sticks + } + } + } + + mexc.Update(pair, &ExchangeState{ + BaseState: BaseState{ + Price: price, + BaseVolume: baseVolume, + Volume: dcrVolume, + Change: priceChange, + Stamp: priceResponse.CloseTime / 1000, + }, + Candlesticks: candlesticks, + Depth: depth, + }) +} diff --git a/exchanges/exchanges_live_test.go b/exchanges/exchanges_live_test.go index e6214be2d..b1879e379 100644 --- a/exchanges/exchanges_live_test.go +++ b/exchanges/exchanges_live_test.go @@ -124,12 +124,6 @@ out: logMissing(token) } - depth, err := bot.QuickDepth(aggregatedOrderbookKey) - if err != nil { - t.Errorf("failed to create aggregated orderbook") - } - log.Infof("aggregated orderbook size: %d kiB", len(depth)/1024) - log.Infof("%d Bitcoin indices available", len(bot.AvailableIndices())) log.Infof("final state is %d kiB", len(bot.StateBytes())/1024) diff --git a/exchanges/exchanges_test.go b/exchanges/exchanges_test.go index 65d538310..e18cf0536 100644 --- a/exchanges/exchanges_test.go +++ b/exchanges/exchanges_test.go @@ -165,8 +165,10 @@ func newTestPoloniexExchange() *PoloniexExchange { return &PoloniexExchange{ CommonExchange: &CommonExchange{ token: Poloniex, - currentState: &ExchangeState{ - BaseState: BaseState{Price: 1}, + currentState: map[CurrencyPair]*ExchangeState{ + CurrencyPairDCRBTC: { + BaseState: BaseState{Price: 1}, + }, }, channels: &BotChannels{ exchange: make(chan *ExchangeUpdate, 2), @@ -205,7 +207,9 @@ func TestPoloniexWebsocket(t *testing.T) { poloniex.wsProcessor = poloniex.processWsMessage poloniex.buys = make(wsOrders) poloniex.asks = make(wsOrders) - poloniex.currentState = &ExchangeState{BaseState: BaseState{Price: 1}} + poloniex.currentState = map[CurrencyPair]*ExchangeState{ + CurrencyPairDCRBTC: {BaseState: BaseState{Price: 1}}, + } poloniex.startWebsocket() time.Sleep(300 * time.Millisecond) poloniex.ws.Close() @@ -288,8 +292,10 @@ func newTestDex() *DecredDEX { return &DecredDEX{ CommonExchange: &CommonExchange{ token: DexDotDecred, - currentState: &ExchangeState{ - BaseState: BaseState{Price: 1}, + currentState: map[CurrencyPair]*ExchangeState{ + CurrencyPairDCRBTC: { + BaseState: BaseState{Price: 1}, + }, }, channels: &BotChannels{ exchange: make(chan *ExchangeUpdate, 2), @@ -396,7 +402,9 @@ func TestDecredDEX(t *testing.T) { dcr.wsProcessor = dcr.processWsMessage dcr.buys = make(wsOrders) dcr.asks = make(wsOrders) - dcr.currentState = &ExchangeState{BaseState: BaseState{Price: 1}} + dcr.currentState = map[CurrencyPair]*ExchangeState{ + CurrencyPairDCRBTC: {BaseState: BaseState{Price: 1}}, + } dcr.startWebsocket() defer dcr.ws.Close() diff --git a/exchanges/go.mod b/exchanges/go.mod index 06ff2d91f..f2d9e72fd 100644 --- a/exchanges/go.mod +++ b/exchanges/go.mod @@ -6,9 +6,9 @@ require ( decred.org/dcrdex v0.6.1 github.com/decred/dcrd/dcrutil/v4 v4.0.1 github.com/decred/slog v1.2.0 - github.com/golang/protobuf v1.5.3 github.com/gorilla/websocket v1.5.0 - google.golang.org/grpc v1.61.0 + google.golang.org/grpc v1.64.1 + google.golang.org/protobuf v1.33.0 ) require ( @@ -95,8 +95,9 @@ require ( github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.2.0 // indirect @@ -157,16 +158,15 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zquestz/grab v0.0.0-20190224022517-abcee96e61b1 // indirect go.etcd.io/bbolt v1.3.7-0.20220130032806-d5db64bdbfde // indirect - golang.org/x/crypto v0.15.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.14.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect lukechampine.com/blake3 v1.2.1 // indirect diff --git a/exchanges/go.sum b/exchanges/go.sum index 02d05114f..042993c3f 100644 --- a/exchanges/go.sum +++ b/exchanges/go.sum @@ -528,8 +528,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -588,8 +588,8 @@ github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4Mgqvf github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.3.8/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= @@ -1275,8 +1275,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1382,8 +1382,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1406,8 +1406,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1500,15 +1500,15 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1519,8 +1519,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1709,9 +1709,8 @@ google.golang.org/genproto v0.0.0-20210426193834-eac7f76ac494/go.mod h1:P3QM42oQ google.golang.org/genproto v0.0.0-20210521181308-5ccab8a35a9a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 h1:q1kiSVscqoDeqTF27eQ2NnLLDmqF0I373qQNXYMy0fo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= @@ -1746,8 +1745,8 @@ google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1765,8 +1764,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/exchanges/rateserver/config.go b/exchanges/rateserver/config.go index 3bed3906a..a4f8f7ab5 100644 --- a/exchanges/rateserver/config.go +++ b/exchanges/rateserver/config.go @@ -34,7 +34,7 @@ type config struct { LogPath string `long:"logpath" description:"Directory to log output. ([appdir]/logs/)" env:"DCRRATES_LOG_PATH"` LogLevel string `long:"loglevel" description:"Logging level {trace, debug, info, warn, error, critical}" env:"DCRRATES_LOG_LEVEL"` DisabledExchanges string `long:"disable-exchange" description:"Exchanges to disable. See /exchanges/exchanges.go for available exchanges. Use a comma to separate multiple exchanges" env:"DCRRATES_DISABLE_EXCHANGES"` - ExchangeCurrency string `long:"exchange-currency" description:"The default bitcoin price index. A 3-letter currency code." env:"DCRRATES_EXCHANGE_INDEX"` + ExchangeCurrency string `long:"exchange-currency" description:"The default {bitcoin, usdt} price index. A 3-letter currency code." env:"DCRRATES_EXCHANGE_INDEX"` ExchangeRefresh string `long:"exchange-refresh" description:"Time between API calls for exchange data. See (ExchangeBotConfig).DataExpiry." env:"DCRRATES_EXCHANGE_REFRESH"` ExchangeExpiry string `long:"exchange-expiry" description:"Maximum age before exchange data is discarded. See (ExchangeBotConfig).RequestExpiry." env:"DCRRATES_EXCHANGE_EXPIRY"` CertificatePath string `long:"tlscert" description:"Path to the TLS certificate. Will be created if it doesn't already exist. ([appdir]/rpc.cert)" env:"DCRRATES_EXCHANGE_EXPIRY"` diff --git a/exchanges/rateserver/dcrrates-server_test.go b/exchanges/rateserver/dcrrates-server_test.go index 6cc563b12..a0dec7d9e 100644 --- a/exchanges/rateserver/dcrrates-server_test.go +++ b/exchanges/rateserver/dcrrates-server_test.go @@ -24,23 +24,54 @@ func TestAddDeleteClient(t *testing.T) { } } -type clientStub struct{} +type clientStub struct { + dcrExchanges map[string]map[exchanges.CurrencyPair]*exchanges.ExchangeState +} + +func (c *clientStub) SendExchangeUpdate(update *dcrrates.ExchangeRateUpdate) error { + if c.dcrExchanges == nil { + c.dcrExchanges = make(map[string]map[exchanges.CurrencyPair]*exchanges.ExchangeState) + } + + if c.dcrExchanges[update.Token] == nil { + c.dcrExchanges[update.Token] = make(map[exchanges.CurrencyPair]*exchanges.ExchangeState) + } + + currencyPair := exchanges.CurrencyPair(update.CurrencyPair) + c.dcrExchanges[update.Token][currencyPair] = &exchanges.ExchangeState{ + BaseState: exchanges.BaseState{ + Price: update.GetPrice(), + BaseVolume: update.GetBaseVolume(), + Volume: update.GetVolume(), + Change: update.GetChange(), + Stamp: update.GetStamp(), + }, + } -func (clientStub) SendExchangeUpdate(*dcrrates.ExchangeRateUpdate) error { return nil } -func (clientStub) Stream() GRPCStream { +func (c *clientStub) Stream() GRPCStream { return nil } func TestSendStateList(t *testing.T) { - updates := make(map[string]*exchanges.ExchangeState) - updates["DummyToken"] = &exchanges.ExchangeState{} - err := sendStateList(clientStub{}, updates) + updates := make(map[string]map[exchanges.CurrencyPair]*exchanges.ExchangeState) + currencyPair := exchanges.CurrencyPairDCRBTC + xcToken := "DummyToken" + updates[xcToken] = map[exchanges.CurrencyPair]*exchanges.ExchangeState{ + currencyPair: {}, + } + + client := &clientStub{} + err := sendStateList(client, updates) if err != nil { t.Fatalf("Error sending exchange states: %v", err) } + + if client.dcrExchanges[xcToken][currencyPair] == nil { + t.Fatalf("expected at least one exchange state for currency pair %s", currencyPair) + } } type certWriterStub struct { diff --git a/exchanges/rateserver/go.mod b/exchanges/rateserver/go.mod index de4e6de05..df670b185 100644 --- a/exchanges/rateserver/go.mod +++ b/exchanges/rateserver/go.mod @@ -11,7 +11,7 @@ require ( github.com/decred/slog v1.2.0 github.com/jessevdk/go-flags v1.5.0 github.com/jrick/logrotate v1.0.0 - google.golang.org/grpc v1.61.0 + google.golang.org/grpc v1.64.1 ) require ( @@ -98,9 +98,9 @@ require ( github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.3.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect @@ -161,16 +161,16 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zquestz/grab v0.0.0-20190224022517-abcee96e61b1 // indirect go.etcd.io/bbolt v1.3.7-0.20220130032806-d5db64bdbfde // indirect - golang.org/x/crypto v0.15.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.14.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect lukechampine.com/blake3 v1.2.1 // indirect diff --git a/exchanges/rateserver/go.sum b/exchanges/rateserver/go.sum index 5a3322e7b..dc00ff607 100644 --- a/exchanges/rateserver/go.sum +++ b/exchanges/rateserver/go.sum @@ -528,8 +528,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -588,8 +588,8 @@ github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4Mgqvf github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.3.8/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= @@ -1276,8 +1276,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1383,8 +1383,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1407,8 +1407,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1501,15 +1501,15 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1520,8 +1520,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1711,8 +1711,8 @@ google.golang.org/genproto v0.0.0-20210521181308-5ccab8a35a9a/go.mod h1:P3QM42oQ google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= @@ -1747,8 +1747,8 @@ google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1766,8 +1766,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/exchanges/rateserver/main.go b/exchanges/rateserver/main.go index 144b3c5f2..1d2c8eee7 100644 --- a/exchanges/rateserver/main.go +++ b/exchanges/rateserver/main.go @@ -64,7 +64,7 @@ func main() { botCfg := exchanges.ExchangeBotConfig{ DataExpiry: cfg.ExchangeRefresh, RequestExpiry: cfg.ExchangeExpiry, - BtcIndex: cfg.ExchangeCurrency, + Index: cfg.ExchangeCurrency, } if cfg.DisabledExchanges != "" { botCfg.Disabled = strings.Split(cfg.DisabledExchanges, ",") @@ -101,10 +101,12 @@ func main() { grpcServer := grpc.NewServer(grpc.Creds(creds)) dcrrates.RegisterDCRRatesServer(grpcServer, rateServer) - printUpdate := func(token string) { - msg := fmt.Sprintf("Update received from %s", token) + log.Infof("ExchangeBot listening on %s", listener.Addr()) + + printUpdate := func(token string, pair exchanges.CurrencyPair) { + msg := fmt.Sprintf("%s: Update received from %s", pair, token) if !xcBot.IsFailed() { - msg += fmt.Sprintf(". Current price: %.2f %s", xcBot.Price(), xcBot.BtcIndex) + msg += fmt.Sprintf(". Current price: %.2f %s", xcBot.Price(), xcBot.Index) } log.Infof(msg) } @@ -128,13 +130,14 @@ func main() { case <-killSwitch: break out case update := <-xcSignals.Exchange: - printUpdate(update.Token) + printUpdate(update.Token, update.CurrencyPair) sendUpdate(makeExchangeRateUpdate(update)) case update := <-xcSignals.Index: - printUpdate(update.Token) + printUpdate(update.Token, update.CurrencyPair) sendUpdate(&dcrrates.ExchangeRateUpdate{ - Token: update.Token, - Indices: update.Indices, + Token: update.Token, + CurrencyPair: update.CurrencyPair.String(), + Indices: update.Indices, }) case <-xcSignals.Quit: log.Infof("ExchangeBot Quit signal received.") diff --git a/exchanges/rateserver/types.go b/exchanges/rateserver/types.go index 4c3b789bd..b67e8ec9a 100644 --- a/exchanges/rateserver/types.go +++ b/exchanges/rateserver/types.go @@ -20,10 +20,12 @@ var streamCounter StreamID // RateServer manages the data sources and client subscriptions. type RateServer struct { - btcIndex string + index string xcBot *exchanges.ExchangeBot clientLock *sync.RWMutex clients map[StreamID]RateClient + + dcrrates.UnimplementedDCRRatesServer } // RateClient is an interface for rateClient to enable testing the server via @@ -36,7 +38,7 @@ type RateClient interface { // NewRateServer is a constructor for a RateServer. func NewRateServer(index string, xcBot *exchanges.ExchangeBot) *RateServer { return &RateServer{ - btcIndex: index, + index: index, clientLock: new(sync.RWMutex), clients: make(map[StreamID]RateClient), xcBot: xcBot, @@ -51,15 +53,18 @@ type GRPCStream interface { // sendStateList is a helper for parsing the ExchangeBotState when a new client // subscription is received. -func sendStateList(client RateClient, states map[string]*exchanges.ExchangeState) (err error) { - for token, state := range states { - err = client.SendExchangeUpdate(makeExchangeRateUpdate(&exchanges.ExchangeUpdate{ - Token: token, - State: state, - })) - if err != nil { - log.Errorf("SendExchangeUpdate error for %s: %v", token, err) - return +func sendStateList(client RateClient, states map[string]map[exchanges.CurrencyPair]*exchanges.ExchangeState) (err error) { + for token, xcStates := range states { + for pair, state := range xcStates { + err = client.SendExchangeUpdate(makeExchangeRateUpdate(&exchanges.ExchangeUpdate{ + Token: token, + CurrencyPair: pair, + State: state, + })) + if err != nil { + log.Errorf("SendExchangeUpdate error for %s: %v", token, err) + return + } } } return @@ -78,8 +83,8 @@ func (server *RateServer) SubscribeExchanges(hello *dcrrates.ExchangeSubscriptio func (server *RateServer) ReallySubscribeExchanges(hello *dcrrates.ExchangeSubscription, stream GRPCStream) (err error) { // For now, require the ExchangeBot clients to have the same base currency. // ToDo: Allow any index. - if hello.BtcIndex != server.btcIndex { - return fmt.Errorf("Exchange subscription has wrong BTC index. Given: %s, Required: %s", hello.BtcIndex, server.btcIndex) + if hello.Index != server.index { + return fmt.Errorf("Exchange subscription has wrong index. Given: %s, Required: %s", hello.Index, server.index) } // Save the client for use in the main loop. client, sid := server.addClient(stream, hello) @@ -94,22 +99,28 @@ func (server *RateServer) ReallySubscribeExchanges(hello *dcrrates.ExchangeSubsc } state := server.xcBot.State() - // Send Decred exchanges. - err = sendStateList(client, state.DcrBtc) - if err != nil { - return err - } - // Send Bitcoin-fiat indices. - for token := range state.FiatIndices { - err = client.SendExchangeUpdate(&dcrrates.ExchangeRateUpdate{ - Token: token, - Indices: server.xcBot.Indices(token), - }) + if state != nil { + // Send Decred exchanges. + err = sendStateList(client, state.DCRExchanges) if err != nil { - log.Errorf("Error encountered while sending fiat indices to client at %s: %v", clientAddr, err) - // Assuming the Done channel will be closed on error, no further iteration - // is necessary. - break + return err + } + // Send Bitcoin-fiat indices. + for token := range state.FiatIndices { + currencyIndexes := server.xcBot.Indices(token) + for currencyPair, indices := range currencyIndexes { + err = client.SendExchangeUpdate(&dcrrates.ExchangeRateUpdate{ + Token: token, + Indices: indices, + CurrencyPair: string(currencyPair), + }) + if err != nil { + log.Errorf("Error encountered while sending fiat indices to client at %s: %v", clientAddr, err) + // Assuming the Done channel will be closed on error, no further iteration + // is necessary. + break + } + } } } @@ -158,12 +169,13 @@ func NewRateClient(stream GRPCStream, exchanges []string) RateClient { func makeExchangeRateUpdate(update *exchanges.ExchangeUpdate) *dcrrates.ExchangeRateUpdate { state := update.State protoUpdate := &dcrrates.ExchangeRateUpdate{ - Token: update.Token, - Price: state.Price, - BaseVolume: state.BaseVolume, - Volume: state.Volume, - Change: state.Change, - Stamp: state.Stamp, + CurrencyPair: update.CurrencyPair.String(), + Token: update.Token, + Price: state.Price, + BaseVolume: state.BaseVolume, + Volume: state.Volume, + Change: state.Change, + Stamp: state.Stamp, } if state.Candlesticks != nil { protoUpdate.Candlesticks = make([]*dcrrates.ExchangeRateUpdate_Candlesticks, 0, len(state.Candlesticks)) diff --git a/exchanges/ratesproto/dcrrates.pb.go b/exchanges/ratesproto/dcrrates.pb.go index 5e39e864e..e8db1fa7b 100644 --- a/exchanges/ratesproto/dcrrates.pb.go +++ b/exchanges/ratesproto/dcrrates.pb.go @@ -1,548 +1,669 @@ // Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v5.27.3 // source: dcrrates.proto -package dcrrates +package __ import ( - context "context" - fmt "fmt" - proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" - math "math" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) type ExchangeSubscription struct { - BtcIndex string `protobuf:"bytes,1,opt,name=btcIndex,proto3" json:"btcIndex,omitempty"` - Exchanges []string `protobuf:"bytes,2,rep,name=exchanges,proto3" json:"exchanges,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields -func (m *ExchangeSubscription) Reset() { *m = ExchangeSubscription{} } -func (m *ExchangeSubscription) String() string { return proto.CompactTextString(m) } -func (*ExchangeSubscription) ProtoMessage() {} -func (*ExchangeSubscription) Descriptor() ([]byte, []int) { - return fileDescriptor_5ecba6b7271820f3, []int{0} + Index string `protobuf:"bytes,1,opt,name=index,proto3" json:"index,omitempty"` + Exchanges []string `protobuf:"bytes,2,rep,name=exchanges,proto3" json:"exchanges,omitempty"` } -func (m *ExchangeSubscription) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExchangeSubscription.Unmarshal(m, b) -} -func (m *ExchangeSubscription) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExchangeSubscription.Marshal(b, m, deterministic) -} -func (m *ExchangeSubscription) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExchangeSubscription.Merge(m, src) +func (x *ExchangeSubscription) Reset() { + *x = ExchangeSubscription{} + if protoimpl.UnsafeEnabled { + mi := &file_dcrrates_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } -func (m *ExchangeSubscription) XXX_Size() int { - return xxx_messageInfo_ExchangeSubscription.Size(m) + +func (x *ExchangeSubscription) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *ExchangeSubscription) XXX_DiscardUnknown() { - xxx_messageInfo_ExchangeSubscription.DiscardUnknown(m) + +func (*ExchangeSubscription) ProtoMessage() {} + +func (x *ExchangeSubscription) ProtoReflect() protoreflect.Message { + mi := &file_dcrrates_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_ExchangeSubscription proto.InternalMessageInfo +// Deprecated: Use ExchangeSubscription.ProtoReflect.Descriptor instead. +func (*ExchangeSubscription) Descriptor() ([]byte, []int) { + return file_dcrrates_proto_rawDescGZIP(), []int{0} +} -func (m *ExchangeSubscription) GetBtcIndex() string { - if m != nil { - return m.BtcIndex +func (x *ExchangeSubscription) GetIndex() string { + if x != nil { + return x.Index } return "" } -func (m *ExchangeSubscription) GetExchanges() []string { - if m != nil { - return m.Exchanges +func (x *ExchangeSubscription) GetExchanges() []string { + if x != nil { + return x.Exchanges } return nil } type ExchangeRateUpdate struct { - Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - Price float64 `protobuf:"fixed64,2,opt,name=price,proto3" json:"price,omitempty"` - BaseVolume float64 `protobuf:"fixed64,3,opt,name=baseVolume,proto3" json:"baseVolume,omitempty"` - Volume float64 `protobuf:"fixed64,4,opt,name=volume,proto3" json:"volume,omitempty"` - Change float64 `protobuf:"fixed64,5,opt,name=change,proto3" json:"change,omitempty"` - Stamp int64 `protobuf:"varint,6,opt,name=stamp,proto3" json:"stamp,omitempty"` - Indices map[string]float64 `protobuf:"bytes,7,rep,name=indices,proto3" json:"indices,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"fixed64,2,opt,name=value,proto3"` - Depth *ExchangeRateUpdate_DepthData `protobuf:"bytes,8,opt,name=depth,proto3" json:"depth,omitempty"` - Candlesticks []*ExchangeRateUpdate_Candlesticks `protobuf:"bytes,9,rep,name=candlesticks,proto3" json:"candlesticks,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *ExchangeRateUpdate) Reset() { *m = ExchangeRateUpdate{} } -func (m *ExchangeRateUpdate) String() string { return proto.CompactTextString(m) } -func (*ExchangeRateUpdate) ProtoMessage() {} -func (*ExchangeRateUpdate) Descriptor() ([]byte, []int) { - return fileDescriptor_5ecba6b7271820f3, []int{1} + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + Price float64 `protobuf:"fixed64,2,opt,name=price,proto3" json:"price,omitempty"` + BaseVolume float64 `protobuf:"fixed64,3,opt,name=baseVolume,proto3" json:"baseVolume,omitempty"` + Volume float64 `protobuf:"fixed64,4,opt,name=volume,proto3" json:"volume,omitempty"` + Change float64 `protobuf:"fixed64,5,opt,name=change,proto3" json:"change,omitempty"` + Stamp int64 `protobuf:"varint,6,opt,name=stamp,proto3" json:"stamp,omitempty"` + Indices map[string]float64 `protobuf:"bytes,7,rep,name=indices,proto3" json:"indices,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"fixed64,2,opt,name=value,proto3"` + Depth *ExchangeRateUpdate_DepthData `protobuf:"bytes,8,opt,name=depth,proto3" json:"depth,omitempty"` + Candlesticks []*ExchangeRateUpdate_Candlesticks `protobuf:"bytes,9,rep,name=candlesticks,proto3" json:"candlesticks,omitempty"` + CurrencyPair string `protobuf:"bytes,10,opt,name=currencyPair,proto3" json:"currencyPair,omitempty"` +} + +func (x *ExchangeRateUpdate) Reset() { + *x = ExchangeRateUpdate{} + if protoimpl.UnsafeEnabled { + mi := &file_dcrrates_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } -func (m *ExchangeRateUpdate) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExchangeRateUpdate.Unmarshal(m, b) -} -func (m *ExchangeRateUpdate) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExchangeRateUpdate.Marshal(b, m, deterministic) -} -func (m *ExchangeRateUpdate) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExchangeRateUpdate.Merge(m, src) +func (x *ExchangeRateUpdate) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *ExchangeRateUpdate) XXX_Size() int { - return xxx_messageInfo_ExchangeRateUpdate.Size(m) -} -func (m *ExchangeRateUpdate) XXX_DiscardUnknown() { - xxx_messageInfo_ExchangeRateUpdate.DiscardUnknown(m) + +func (*ExchangeRateUpdate) ProtoMessage() {} + +func (x *ExchangeRateUpdate) ProtoReflect() protoreflect.Message { + mi := &file_dcrrates_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_ExchangeRateUpdate proto.InternalMessageInfo +// Deprecated: Use ExchangeRateUpdate.ProtoReflect.Descriptor instead. +func (*ExchangeRateUpdate) Descriptor() ([]byte, []int) { + return file_dcrrates_proto_rawDescGZIP(), []int{1} +} -func (m *ExchangeRateUpdate) GetToken() string { - if m != nil { - return m.Token +func (x *ExchangeRateUpdate) GetToken() string { + if x != nil { + return x.Token } return "" } -func (m *ExchangeRateUpdate) GetPrice() float64 { - if m != nil { - return m.Price +func (x *ExchangeRateUpdate) GetPrice() float64 { + if x != nil { + return x.Price } return 0 } -func (m *ExchangeRateUpdate) GetBaseVolume() float64 { - if m != nil { - return m.BaseVolume +func (x *ExchangeRateUpdate) GetBaseVolume() float64 { + if x != nil { + return x.BaseVolume } return 0 } -func (m *ExchangeRateUpdate) GetVolume() float64 { - if m != nil { - return m.Volume +func (x *ExchangeRateUpdate) GetVolume() float64 { + if x != nil { + return x.Volume } return 0 } -func (m *ExchangeRateUpdate) GetChange() float64 { - if m != nil { - return m.Change +func (x *ExchangeRateUpdate) GetChange() float64 { + if x != nil { + return x.Change } return 0 } -func (m *ExchangeRateUpdate) GetStamp() int64 { - if m != nil { - return m.Stamp +func (x *ExchangeRateUpdate) GetStamp() int64 { + if x != nil { + return x.Stamp } return 0 } -func (m *ExchangeRateUpdate) GetIndices() map[string]float64 { - if m != nil { - return m.Indices +func (x *ExchangeRateUpdate) GetIndices() map[string]float64 { + if x != nil { + return x.Indices } return nil } -func (m *ExchangeRateUpdate) GetDepth() *ExchangeRateUpdate_DepthData { - if m != nil { - return m.Depth +func (x *ExchangeRateUpdate) GetDepth() *ExchangeRateUpdate_DepthData { + if x != nil { + return x.Depth } return nil } -func (m *ExchangeRateUpdate) GetCandlesticks() []*ExchangeRateUpdate_Candlesticks { - if m != nil { - return m.Candlesticks +func (x *ExchangeRateUpdate) GetCandlesticks() []*ExchangeRateUpdate_Candlesticks { + if x != nil { + return x.Candlesticks } return nil } -type ExchangeRateUpdate_DepthPoint struct { - Quantity float64 `protobuf:"fixed64,1,opt,name=quantity,proto3" json:"quantity,omitempty"` - Price float64 `protobuf:"fixed64,2,opt,name=price,proto3" json:"price,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` +func (x *ExchangeRateUpdate) GetCurrencyPair() string { + if x != nil { + return x.CurrencyPair + } + return "" } -func (m *ExchangeRateUpdate_DepthPoint) Reset() { *m = ExchangeRateUpdate_DepthPoint{} } -func (m *ExchangeRateUpdate_DepthPoint) String() string { return proto.CompactTextString(m) } -func (*ExchangeRateUpdate_DepthPoint) ProtoMessage() {} -func (*ExchangeRateUpdate_DepthPoint) Descriptor() ([]byte, []int) { - return fileDescriptor_5ecba6b7271820f3, []int{1, 1} -} +type ExchangeRateUpdate_DepthPoint struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields -func (m *ExchangeRateUpdate_DepthPoint) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExchangeRateUpdate_DepthPoint.Unmarshal(m, b) -} -func (m *ExchangeRateUpdate_DepthPoint) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExchangeRateUpdate_DepthPoint.Marshal(b, m, deterministic) + Quantity float64 `protobuf:"fixed64,1,opt,name=quantity,proto3" json:"quantity,omitempty"` + Price float64 `protobuf:"fixed64,2,opt,name=price,proto3" json:"price,omitempty"` } -func (m *ExchangeRateUpdate_DepthPoint) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExchangeRateUpdate_DepthPoint.Merge(m, src) + +func (x *ExchangeRateUpdate_DepthPoint) Reset() { + *x = ExchangeRateUpdate_DepthPoint{} + if protoimpl.UnsafeEnabled { + mi := &file_dcrrates_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } -func (m *ExchangeRateUpdate_DepthPoint) XXX_Size() int { - return xxx_messageInfo_ExchangeRateUpdate_DepthPoint.Size(m) + +func (x *ExchangeRateUpdate_DepthPoint) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *ExchangeRateUpdate_DepthPoint) XXX_DiscardUnknown() { - xxx_messageInfo_ExchangeRateUpdate_DepthPoint.DiscardUnknown(m) + +func (*ExchangeRateUpdate_DepthPoint) ProtoMessage() {} + +func (x *ExchangeRateUpdate_DepthPoint) ProtoReflect() protoreflect.Message { + mi := &file_dcrrates_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_ExchangeRateUpdate_DepthPoint proto.InternalMessageInfo +// Deprecated: Use ExchangeRateUpdate_DepthPoint.ProtoReflect.Descriptor instead. +func (*ExchangeRateUpdate_DepthPoint) Descriptor() ([]byte, []int) { + return file_dcrrates_proto_rawDescGZIP(), []int{1, 1} +} -func (m *ExchangeRateUpdate_DepthPoint) GetQuantity() float64 { - if m != nil { - return m.Quantity +func (x *ExchangeRateUpdate_DepthPoint) GetQuantity() float64 { + if x != nil { + return x.Quantity } return 0 } -func (m *ExchangeRateUpdate_DepthPoint) GetPrice() float64 { - if m != nil { - return m.Price +func (x *ExchangeRateUpdate_DepthPoint) GetPrice() float64 { + if x != nil { + return x.Price } return 0 } type ExchangeRateUpdate_DepthData struct { - Time int64 `protobuf:"varint,1,opt,name=time,proto3" json:"time,omitempty"` - Bids []*ExchangeRateUpdate_DepthPoint `protobuf:"bytes,2,rep,name=bids,proto3" json:"bids,omitempty"` - Asks []*ExchangeRateUpdate_DepthPoint `protobuf:"bytes,3,rep,name=asks,proto3" json:"asks,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields -func (m *ExchangeRateUpdate_DepthData) Reset() { *m = ExchangeRateUpdate_DepthData{} } -func (m *ExchangeRateUpdate_DepthData) String() string { return proto.CompactTextString(m) } -func (*ExchangeRateUpdate_DepthData) ProtoMessage() {} -func (*ExchangeRateUpdate_DepthData) Descriptor() ([]byte, []int) { - return fileDescriptor_5ecba6b7271820f3, []int{1, 2} + Time int64 `protobuf:"varint,1,opt,name=time,proto3" json:"time,omitempty"` + Bids []*ExchangeRateUpdate_DepthPoint `protobuf:"bytes,2,rep,name=bids,proto3" json:"bids,omitempty"` + Asks []*ExchangeRateUpdate_DepthPoint `protobuf:"bytes,3,rep,name=asks,proto3" json:"asks,omitempty"` } -func (m *ExchangeRateUpdate_DepthData) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExchangeRateUpdate_DepthData.Unmarshal(m, b) -} -func (m *ExchangeRateUpdate_DepthData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExchangeRateUpdate_DepthData.Marshal(b, m, deterministic) -} -func (m *ExchangeRateUpdate_DepthData) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExchangeRateUpdate_DepthData.Merge(m, src) +func (x *ExchangeRateUpdate_DepthData) Reset() { + *x = ExchangeRateUpdate_DepthData{} + if protoimpl.UnsafeEnabled { + mi := &file_dcrrates_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } -func (m *ExchangeRateUpdate_DepthData) XXX_Size() int { - return xxx_messageInfo_ExchangeRateUpdate_DepthData.Size(m) + +func (x *ExchangeRateUpdate_DepthData) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *ExchangeRateUpdate_DepthData) XXX_DiscardUnknown() { - xxx_messageInfo_ExchangeRateUpdate_DepthData.DiscardUnknown(m) + +func (*ExchangeRateUpdate_DepthData) ProtoMessage() {} + +func (x *ExchangeRateUpdate_DepthData) ProtoReflect() protoreflect.Message { + mi := &file_dcrrates_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_ExchangeRateUpdate_DepthData proto.InternalMessageInfo +// Deprecated: Use ExchangeRateUpdate_DepthData.ProtoReflect.Descriptor instead. +func (*ExchangeRateUpdate_DepthData) Descriptor() ([]byte, []int) { + return file_dcrrates_proto_rawDescGZIP(), []int{1, 2} +} -func (m *ExchangeRateUpdate_DepthData) GetTime() int64 { - if m != nil { - return m.Time +func (x *ExchangeRateUpdate_DepthData) GetTime() int64 { + if x != nil { + return x.Time } return 0 } -func (m *ExchangeRateUpdate_DepthData) GetBids() []*ExchangeRateUpdate_DepthPoint { - if m != nil { - return m.Bids +func (x *ExchangeRateUpdate_DepthData) GetBids() []*ExchangeRateUpdate_DepthPoint { + if x != nil { + return x.Bids } return nil } -func (m *ExchangeRateUpdate_DepthData) GetAsks() []*ExchangeRateUpdate_DepthPoint { - if m != nil { - return m.Asks +func (x *ExchangeRateUpdate_DepthData) GetAsks() []*ExchangeRateUpdate_DepthPoint { + if x != nil { + return x.Asks } return nil } type ExchangeRateUpdate_Candlestick struct { - High float64 `protobuf:"fixed64,1,opt,name=high,proto3" json:"high,omitempty"` - Low float64 `protobuf:"fixed64,2,opt,name=low,proto3" json:"low,omitempty"` - Open float64 `protobuf:"fixed64,3,opt,name=open,proto3" json:"open,omitempty"` - Close float64 `protobuf:"fixed64,4,opt,name=close,proto3" json:"close,omitempty"` - Volume float64 `protobuf:"fixed64,5,opt,name=volume,proto3" json:"volume,omitempty"` - Start int64 `protobuf:"varint,6,opt,name=start,proto3" json:"start,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *ExchangeRateUpdate_Candlestick) Reset() { *m = ExchangeRateUpdate_Candlestick{} } -func (m *ExchangeRateUpdate_Candlestick) String() string { return proto.CompactTextString(m) } -func (*ExchangeRateUpdate_Candlestick) ProtoMessage() {} -func (*ExchangeRateUpdate_Candlestick) Descriptor() ([]byte, []int) { - return fileDescriptor_5ecba6b7271820f3, []int{1, 3} + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + High float64 `protobuf:"fixed64,1,opt,name=high,proto3" json:"high,omitempty"` + Low float64 `protobuf:"fixed64,2,opt,name=low,proto3" json:"low,omitempty"` + Open float64 `protobuf:"fixed64,3,opt,name=open,proto3" json:"open,omitempty"` + Close float64 `protobuf:"fixed64,4,opt,name=close,proto3" json:"close,omitempty"` + Volume float64 `protobuf:"fixed64,5,opt,name=volume,proto3" json:"volume,omitempty"` + Start int64 `protobuf:"varint,6,opt,name=start,proto3" json:"start,omitempty"` +} + +func (x *ExchangeRateUpdate_Candlestick) Reset() { + *x = ExchangeRateUpdate_Candlestick{} + if protoimpl.UnsafeEnabled { + mi := &file_dcrrates_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } -func (m *ExchangeRateUpdate_Candlestick) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExchangeRateUpdate_Candlestick.Unmarshal(m, b) -} -func (m *ExchangeRateUpdate_Candlestick) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExchangeRateUpdate_Candlestick.Marshal(b, m, deterministic) -} -func (m *ExchangeRateUpdate_Candlestick) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExchangeRateUpdate_Candlestick.Merge(m, src) -} -func (m *ExchangeRateUpdate_Candlestick) XXX_Size() int { - return xxx_messageInfo_ExchangeRateUpdate_Candlestick.Size(m) -} -func (m *ExchangeRateUpdate_Candlestick) XXX_DiscardUnknown() { - xxx_messageInfo_ExchangeRateUpdate_Candlestick.DiscardUnknown(m) +func (x *ExchangeRateUpdate_Candlestick) String() string { + return protoimpl.X.MessageStringOf(x) } -var xxx_messageInfo_ExchangeRateUpdate_Candlestick proto.InternalMessageInfo +func (*ExchangeRateUpdate_Candlestick) ProtoMessage() {} -func (m *ExchangeRateUpdate_Candlestick) GetHigh() float64 { - if m != nil { - return m.High +func (x *ExchangeRateUpdate_Candlestick) ProtoReflect() protoreflect.Message { + mi := &file_dcrrates_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (m *ExchangeRateUpdate_Candlestick) GetLow() float64 { - if m != nil { - return m.Low - } - return 0 +// Deprecated: Use ExchangeRateUpdate_Candlestick.ProtoReflect.Descriptor instead. +func (*ExchangeRateUpdate_Candlestick) Descriptor() ([]byte, []int) { + return file_dcrrates_proto_rawDescGZIP(), []int{1, 3} } -func (m *ExchangeRateUpdate_Candlestick) GetOpen() float64 { - if m != nil { - return m.Open +func (x *ExchangeRateUpdate_Candlestick) GetHigh() float64 { + if x != nil { + return x.High } return 0 } -func (m *ExchangeRateUpdate_Candlestick) GetClose() float64 { - if m != nil { - return m.Close +func (x *ExchangeRateUpdate_Candlestick) GetLow() float64 { + if x != nil { + return x.Low } return 0 } -func (m *ExchangeRateUpdate_Candlestick) GetVolume() float64 { - if m != nil { - return m.Volume +func (x *ExchangeRateUpdate_Candlestick) GetOpen() float64 { + if x != nil { + return x.Open } return 0 } -func (m *ExchangeRateUpdate_Candlestick) GetStart() int64 { - if m != nil { - return m.Start +func (x *ExchangeRateUpdate_Candlestick) GetClose() float64 { + if x != nil { + return x.Close } return 0 } -type ExchangeRateUpdate_Candlesticks struct { - Bin string `protobuf:"bytes,1,opt,name=bin,proto3" json:"bin,omitempty"` - Sticks []*ExchangeRateUpdate_Candlestick `protobuf:"bytes,2,rep,name=sticks,proto3" json:"sticks,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *ExchangeRateUpdate_Candlesticks) Reset() { *m = ExchangeRateUpdate_Candlesticks{} } -func (m *ExchangeRateUpdate_Candlesticks) String() string { return proto.CompactTextString(m) } -func (*ExchangeRateUpdate_Candlesticks) ProtoMessage() {} -func (*ExchangeRateUpdate_Candlesticks) Descriptor() ([]byte, []int) { - return fileDescriptor_5ecba6b7271820f3, []int{1, 4} -} - -func (m *ExchangeRateUpdate_Candlesticks) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExchangeRateUpdate_Candlesticks.Unmarshal(m, b) -} -func (m *ExchangeRateUpdate_Candlesticks) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExchangeRateUpdate_Candlesticks.Marshal(b, m, deterministic) -} -func (m *ExchangeRateUpdate_Candlesticks) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExchangeRateUpdate_Candlesticks.Merge(m, src) -} -func (m *ExchangeRateUpdate_Candlesticks) XXX_Size() int { - return xxx_messageInfo_ExchangeRateUpdate_Candlesticks.Size(m) -} -func (m *ExchangeRateUpdate_Candlesticks) XXX_DiscardUnknown() { - xxx_messageInfo_ExchangeRateUpdate_Candlesticks.DiscardUnknown(m) -} - -var xxx_messageInfo_ExchangeRateUpdate_Candlesticks proto.InternalMessageInfo - -func (m *ExchangeRateUpdate_Candlesticks) GetBin() string { - if m != nil { - return m.Bin +func (x *ExchangeRateUpdate_Candlestick) GetVolume() float64 { + if x != nil { + return x.Volume } - return "" + return 0 } -func (m *ExchangeRateUpdate_Candlesticks) GetSticks() []*ExchangeRateUpdate_Candlestick { - if m != nil { - return m.Sticks +func (x *ExchangeRateUpdate_Candlestick) GetStart() int64 { + if x != nil { + return x.Start } - return nil -} - -func init() { - proto.RegisterType((*ExchangeSubscription)(nil), "dcrrates.ExchangeSubscription") - proto.RegisterType((*ExchangeRateUpdate)(nil), "dcrrates.ExchangeRateUpdate") - proto.RegisterMapType((map[string]float64)(nil), "dcrrates.ExchangeRateUpdate.IndicesEntry") - proto.RegisterType((*ExchangeRateUpdate_DepthPoint)(nil), "dcrrates.ExchangeRateUpdate.DepthPoint") - proto.RegisterType((*ExchangeRateUpdate_DepthData)(nil), "dcrrates.ExchangeRateUpdate.DepthData") - proto.RegisterType((*ExchangeRateUpdate_Candlestick)(nil), "dcrrates.ExchangeRateUpdate.Candlestick") - proto.RegisterType((*ExchangeRateUpdate_Candlesticks)(nil), "dcrrates.ExchangeRateUpdate.Candlesticks") -} - -func init() { proto.RegisterFile("dcrrates.proto", fileDescriptor_5ecba6b7271820f3) } - -var fileDescriptor_5ecba6b7271820f3 = []byte{ - // 501 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0xcb, 0x6a, 0xdc, 0x30, - 0x14, 0x45, 0xe3, 0x79, 0xf9, 0xce, 0x50, 0x8a, 0x08, 0x45, 0x98, 0x10, 0x4c, 0x16, 0xad, 0xbb, - 0x19, 0xca, 0x74, 0x53, 0xd2, 0x52, 0x0a, 0x33, 0x59, 0x64, 0x51, 0x08, 0xea, 0x63, 0x5d, 0xd9, - 0x16, 0x19, 0x31, 0x1e, 0xc9, 0xb5, 0x34, 0x69, 0xb2, 0xed, 0xb6, 0x5f, 0xd0, 0xbf, 0x2d, 0x7a, - 0xd8, 0x75, 0x48, 0x49, 0x27, 0xbb, 0x7b, 0xae, 0x74, 0xee, 0xe3, 0xe8, 0xd8, 0xf0, 0xa4, 0x2c, - 0x9a, 0x86, 0x19, 0xae, 0x17, 0x75, 0xa3, 0x8c, 0xc2, 0xd3, 0x16, 0x9f, 0x5e, 0xc2, 0xd1, 0xf9, - 0x4d, 0xb1, 0x61, 0xf2, 0x8a, 0x7f, 0xda, 0xe7, 0xba, 0x68, 0x44, 0x6d, 0x84, 0x92, 0x38, 0x81, - 0x69, 0x6e, 0x8a, 0x0b, 0x59, 0xf2, 0x1b, 0x82, 0x52, 0x94, 0xc5, 0xb4, 0xc3, 0xf8, 0x18, 0x62, - 0x1e, 0x38, 0x9a, 0x0c, 0xd2, 0x28, 0x8b, 0xe9, 0xdf, 0xc4, 0xe9, 0xcf, 0x09, 0xe0, 0xb6, 0x24, - 0x65, 0x86, 0x7f, 0xa9, 0x4b, 0x66, 0x38, 0x3e, 0x82, 0x91, 0x51, 0x5b, 0x2e, 0x43, 0x35, 0x0f, - 0x6c, 0xb6, 0x6e, 0x44, 0xc1, 0xc9, 0x20, 0x45, 0x19, 0xa2, 0x1e, 0xe0, 0x13, 0x80, 0x9c, 0x69, - 0xfe, 0x55, 0x55, 0xfb, 0x1d, 0x27, 0x91, 0x3b, 0xea, 0x65, 0xf0, 0x33, 0x18, 0x5f, 0xfb, 0xb3, - 0xa1, 0x3b, 0x0b, 0xc8, 0xe6, 0x7d, 0x5f, 0x32, 0xf2, 0x79, 0x8f, 0x6c, 0x17, 0x6d, 0xd8, 0xae, - 0x26, 0xe3, 0x14, 0x65, 0x11, 0xf5, 0x00, 0xaf, 0x60, 0x22, 0x64, 0x29, 0x0a, 0xae, 0xc9, 0x24, - 0x8d, 0xb2, 0xd9, 0xf2, 0xe5, 0xa2, 0x93, 0xe9, 0xfe, 0x02, 0x8b, 0x0b, 0x7f, 0xf7, 0x5c, 0x9a, - 0xe6, 0x96, 0xb6, 0x4c, 0xfc, 0x0e, 0x46, 0x25, 0xaf, 0xcd, 0x86, 0x4c, 0x53, 0x94, 0xcd, 0x96, - 0xcf, 0x1f, 0x2c, 0xb1, 0xb6, 0x37, 0xd7, 0xcc, 0x30, 0xea, 0x49, 0xf8, 0x23, 0xcc, 0x0b, 0x26, - 0xcb, 0x8a, 0x6b, 0x23, 0x8a, 0xad, 0x26, 0xf1, 0x01, 0x73, 0xac, 0x7a, 0x04, 0x7a, 0x87, 0x9e, - 0x9c, 0xc1, 0xbc, 0x3f, 0x25, 0x7e, 0x0a, 0xd1, 0x96, 0xdf, 0x06, 0xc5, 0x6d, 0x68, 0x95, 0xb8, - 0x66, 0xd5, 0xbe, 0xd3, 0xdb, 0x81, 0xb3, 0xc1, 0x1b, 0x94, 0xbc, 0x07, 0x70, 0xe3, 0x5d, 0x2a, - 0x21, 0x8d, 0x7d, 0xfe, 0xef, 0x7b, 0x26, 0x8d, 0x30, 0x9e, 0x8e, 0x68, 0x87, 0xff, 0xfd, 0x66, - 0xc9, 0x6f, 0x04, 0x71, 0xb7, 0x1f, 0xc6, 0x30, 0x34, 0x62, 0xc7, 0x1d, 0x37, 0xa2, 0x2e, 0xc6, - 0x6f, 0x61, 0x98, 0x8b, 0xd2, 0x3b, 0x66, 0xb6, 0x7c, 0xf1, 0x7f, 0xa5, 0xdc, 0x28, 0xd4, 0x91, - 0x2c, 0x99, 0xe9, 0xad, 0x26, 0xd1, 0x23, 0xc9, 0x96, 0x94, 0xfc, 0x42, 0x30, 0xeb, 0xc9, 0x66, - 0xa7, 0xdb, 0x88, 0xab, 0x4d, 0xd8, 0xcc, 0xc5, 0x56, 0xab, 0x4a, 0xfd, 0x08, 0x3b, 0xd9, 0xd0, - 0xde, 0x52, 0x35, 0x97, 0xc1, 0x7f, 0x2e, 0xb6, 0xbb, 0x17, 0x95, 0xd2, 0xad, 0xf1, 0x3c, 0xe8, - 0xf9, 0x71, 0x74, 0xc7, 0x8f, 0xde, 0x77, 0x8d, 0xe9, 0xf9, 0xae, 0x31, 0x49, 0x0e, 0xf3, 0xfe, - 0x1b, 0xda, 0xce, 0xb9, 0x68, 0xbf, 0x0b, 0x1b, 0xe2, 0x0f, 0x30, 0x0e, 0x86, 0xf0, 0x5a, 0x65, - 0x87, 0x1a, 0x82, 0x06, 0xde, 0xf2, 0x1b, 0x4c, 0xd7, 0x2b, 0x6a, 0x2f, 0x69, 0xfc, 0x19, 0x70, - 0xf8, 0xb4, 0x73, 0xde, 0xd2, 0x35, 0x3e, 0xb9, 0x5f, 0xb3, 0xff, 0x03, 0x48, 0x8e, 0x1f, 0xea, - 0xf9, 0x0a, 0xe5, 0x63, 0xf7, 0x27, 0x79, 0xfd, 0x27, 0x00, 0x00, 0xff, 0xff, 0x3c, 0xa6, 0x5c, - 0xed, 0x5b, 0x04, 0x00, 0x00, -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// DCRRatesClient is the client API for DCRRates service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type DCRRatesClient interface { - SubscribeExchanges(ctx context.Context, in *ExchangeSubscription, opts ...grpc.CallOption) (DCRRates_SubscribeExchangesClient, error) + return 0 } -type dCRRatesClient struct { - cc *grpc.ClientConn -} +type ExchangeRateUpdate_Candlesticks struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields -func NewDCRRatesClient(cc *grpc.ClientConn) DCRRatesClient { - return &dCRRatesClient{cc} + Bin string `protobuf:"bytes,1,opt,name=bin,proto3" json:"bin,omitempty"` + Sticks []*ExchangeRateUpdate_Candlestick `protobuf:"bytes,2,rep,name=sticks,proto3" json:"sticks,omitempty"` } -func (c *dCRRatesClient) SubscribeExchanges(ctx context.Context, in *ExchangeSubscription, opts ...grpc.CallOption) (DCRRates_SubscribeExchangesClient, error) { - stream, err := c.cc.NewStream(ctx, &_DCRRates_serviceDesc.Streams[0], "/dcrrates.DCRRates/SubscribeExchanges", opts...) - if err != nil { - return nil, err +func (x *ExchangeRateUpdate_Candlesticks) Reset() { + *x = ExchangeRateUpdate_Candlesticks{} + if protoimpl.UnsafeEnabled { + mi := &file_dcrrates_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } - x := &dCRRatesSubscribeExchangesClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil } -type DCRRates_SubscribeExchangesClient interface { - Recv() (*ExchangeRateUpdate, error) - grpc.ClientStream +func (x *ExchangeRateUpdate_Candlesticks) String() string { + return protoimpl.X.MessageStringOf(x) } -type dCRRatesSubscribeExchangesClient struct { - grpc.ClientStream -} +func (*ExchangeRateUpdate_Candlesticks) ProtoMessage() {} -func (x *dCRRatesSubscribeExchangesClient) Recv() (*ExchangeRateUpdate, error) { - m := new(ExchangeRateUpdate) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err +func (x *ExchangeRateUpdate_Candlesticks) ProtoReflect() protoreflect.Message { + mi := &file_dcrrates_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return m, nil + return mi.MessageOf(x) } -// DCRRatesServer is the server API for DCRRates service. -type DCRRatesServer interface { - SubscribeExchanges(*ExchangeSubscription, DCRRates_SubscribeExchangesServer) error -} - -func RegisterDCRRatesServer(s *grpc.Server, srv DCRRatesServer) { - s.RegisterService(&_DCRRates_serviceDesc, srv) +// Deprecated: Use ExchangeRateUpdate_Candlesticks.ProtoReflect.Descriptor instead. +func (*ExchangeRateUpdate_Candlesticks) Descriptor() ([]byte, []int) { + return file_dcrrates_proto_rawDescGZIP(), []int{1, 4} } -func _DCRRates_SubscribeExchanges_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(ExchangeSubscription) - if err := stream.RecvMsg(m); err != nil { - return err +func (x *ExchangeRateUpdate_Candlesticks) GetBin() string { + if x != nil { + return x.Bin } - return srv.(DCRRatesServer).SubscribeExchanges(m, &dCRRatesSubscribeExchangesServer{stream}) -} - -type DCRRates_SubscribeExchangesServer interface { - Send(*ExchangeRateUpdate) error - grpc.ServerStream + return "" } -type dCRRatesSubscribeExchangesServer struct { - grpc.ServerStream +func (x *ExchangeRateUpdate_Candlesticks) GetSticks() []*ExchangeRateUpdate_Candlestick { + if x != nil { + return x.Sticks + } + return nil } -func (x *dCRRatesSubscribeExchangesServer) Send(m *ExchangeRateUpdate) error { - return x.ServerStream.SendMsg(m) -} +var File_dcrrates_proto protoreflect.FileDescriptor + +var file_dcrrates_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x08, 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x22, 0x4a, 0x0a, 0x14, 0x45, 0x78, + 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x78, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x65, 0x78, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0xa6, 0x07, 0x0a, 0x12, 0x45, 0x78, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x01, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x62, 0x61, 0x73, + 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0a, 0x62, + 0x61, 0x73, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, + 0x75, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, + 0x43, 0x0a, 0x07, 0x69, 0x6e, 0x64, 0x69, 0x63, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x29, 0x2e, 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x45, 0x78, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x49, + 0x6e, 0x64, 0x69, 0x63, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x69, 0x6e, 0x64, + 0x69, 0x63, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x05, 0x64, 0x65, 0x70, 0x74, 0x68, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x45, + 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x2e, 0x44, 0x65, 0x70, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0x52, 0x05, 0x64, 0x65, 0x70, + 0x74, 0x68, 0x12, 0x4d, 0x0a, 0x0c, 0x63, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x74, 0x69, 0x63, + 0x6b, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x64, 0x63, 0x72, 0x72, 0x61, + 0x74, 0x65, 0x73, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x43, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x74, 0x69, + 0x63, 0x6b, 0x73, 0x52, 0x0c, 0x63, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x74, 0x69, 0x63, 0x6b, + 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x50, 0x61, 0x69, + 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, + 0x79, 0x50, 0x61, 0x69, 0x72, 0x1a, 0x3a, 0x0a, 0x0c, 0x49, 0x6e, 0x64, 0x69, 0x63, 0x65, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x1a, 0x3e, 0x0a, 0x0a, 0x44, 0x65, 0x70, 0x74, 0x68, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x70, + 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, + 0x65, 0x1a, 0x99, 0x01, 0x0a, 0x09, 0x44, 0x65, 0x70, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, + 0x69, 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x62, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x27, 0x2e, 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x45, 0x78, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, + 0x44, 0x65, 0x70, 0x74, 0x68, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x04, 0x62, 0x69, 0x64, 0x73, + 0x12, 0x3b, 0x0a, 0x04, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x44, 0x65, 0x70, + 0x74, 0x68, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x04, 0x61, 0x73, 0x6b, 0x73, 0x1a, 0x8b, 0x01, + 0x0a, 0x0b, 0x43, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x74, 0x69, 0x63, 0x6b, 0x12, 0x12, 0x0a, + 0x04, 0x68, 0x69, 0x67, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, 0x68, 0x69, 0x67, + 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x03, + 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x6f, 0x70, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x04, 0x6f, 0x70, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6c, 0x6f, 0x73, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x76, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x1a, 0x62, 0x0a, 0x0c, 0x43, + 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x74, 0x69, 0x63, 0x6b, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x62, + 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x62, 0x69, 0x6e, 0x12, 0x40, 0x0a, + 0x06, 0x73, 0x74, 0x69, 0x63, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, + 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x52, 0x61, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x43, 0x61, 0x6e, 0x64, + 0x6c, 0x65, 0x73, 0x74, 0x69, 0x63, 0x6b, 0x52, 0x06, 0x73, 0x74, 0x69, 0x63, 0x6b, 0x73, 0x32, + 0x60, 0x0a, 0x08, 0x44, 0x43, 0x52, 0x52, 0x61, 0x74, 0x65, 0x73, 0x12, 0x54, 0x0a, 0x12, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x73, 0x12, 0x1e, 0x2e, 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x45, 0x78, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x1a, 0x1c, 0x2e, 0x64, 0x63, 0x72, 0x72, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x45, 0x78, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, + 0x01, 0x42, 0x03, 0x5a, 0x01, 0x2e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_dcrrates_proto_rawDescOnce sync.Once + file_dcrrates_proto_rawDescData = file_dcrrates_proto_rawDesc +) -var _DCRRates_serviceDesc = grpc.ServiceDesc{ - ServiceName: "dcrrates.DCRRates", - HandlerType: (*DCRRatesServer)(nil), - Methods: []grpc.MethodDesc{}, - Streams: []grpc.StreamDesc{ - { - StreamName: "SubscribeExchanges", - Handler: _DCRRates_SubscribeExchanges_Handler, - ServerStreams: true, +func file_dcrrates_proto_rawDescGZIP() []byte { + file_dcrrates_proto_rawDescOnce.Do(func() { + file_dcrrates_proto_rawDescData = protoimpl.X.CompressGZIP(file_dcrrates_proto_rawDescData) + }) + return file_dcrrates_proto_rawDescData +} + +var file_dcrrates_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_dcrrates_proto_goTypes = []any{ + (*ExchangeSubscription)(nil), // 0: dcrrates.ExchangeSubscription + (*ExchangeRateUpdate)(nil), // 1: dcrrates.ExchangeRateUpdate + nil, // 2: dcrrates.ExchangeRateUpdate.IndicesEntry + (*ExchangeRateUpdate_DepthPoint)(nil), // 3: dcrrates.ExchangeRateUpdate.DepthPoint + (*ExchangeRateUpdate_DepthData)(nil), // 4: dcrrates.ExchangeRateUpdate.DepthData + (*ExchangeRateUpdate_Candlestick)(nil), // 5: dcrrates.ExchangeRateUpdate.Candlestick + (*ExchangeRateUpdate_Candlesticks)(nil), // 6: dcrrates.ExchangeRateUpdate.Candlesticks +} +var file_dcrrates_proto_depIdxs = []int32{ + 2, // 0: dcrrates.ExchangeRateUpdate.indices:type_name -> dcrrates.ExchangeRateUpdate.IndicesEntry + 4, // 1: dcrrates.ExchangeRateUpdate.depth:type_name -> dcrrates.ExchangeRateUpdate.DepthData + 6, // 2: dcrrates.ExchangeRateUpdate.candlesticks:type_name -> dcrrates.ExchangeRateUpdate.Candlesticks + 3, // 3: dcrrates.ExchangeRateUpdate.DepthData.bids:type_name -> dcrrates.ExchangeRateUpdate.DepthPoint + 3, // 4: dcrrates.ExchangeRateUpdate.DepthData.asks:type_name -> dcrrates.ExchangeRateUpdate.DepthPoint + 5, // 5: dcrrates.ExchangeRateUpdate.Candlesticks.sticks:type_name -> dcrrates.ExchangeRateUpdate.Candlestick + 0, // 6: dcrrates.DCRRates.SubscribeExchanges:input_type -> dcrrates.ExchangeSubscription + 1, // 7: dcrrates.DCRRates.SubscribeExchanges:output_type -> dcrrates.ExchangeRateUpdate + 7, // [7:8] is the sub-list for method output_type + 6, // [6:7] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_dcrrates_proto_init() } +func file_dcrrates_proto_init() { + if File_dcrrates_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_dcrrates_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*ExchangeSubscription); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_dcrrates_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*ExchangeRateUpdate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_dcrrates_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*ExchangeRateUpdate_DepthPoint); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_dcrrates_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*ExchangeRateUpdate_DepthData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_dcrrates_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*ExchangeRateUpdate_Candlestick); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_dcrrates_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*ExchangeRateUpdate_Candlesticks); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_dcrrates_proto_rawDesc, + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, }, - }, - Metadata: "dcrrates.proto", + GoTypes: file_dcrrates_proto_goTypes, + DependencyIndexes: file_dcrrates_proto_depIdxs, + MessageInfos: file_dcrrates_proto_msgTypes, + }.Build() + File_dcrrates_proto = out.File + file_dcrrates_proto_rawDesc = nil + file_dcrrates_proto_goTypes = nil + file_dcrrates_proto_depIdxs = nil } diff --git a/exchanges/ratesproto/dcrrates.proto b/exchanges/ratesproto/dcrrates.proto index 36aa78249..1f2fe9aa3 100644 --- a/exchanges/ratesproto/dcrrates.proto +++ b/exchanges/ratesproto/dcrrates.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package dcrrates; +option go_package = "."; + // DCRRates takes a subscription from a client and pushes data as its received // from external sources. service DCRRates { @@ -9,7 +11,7 @@ service DCRRates { } message ExchangeSubscription { - string btcIndex = 1; + string index = 1; repeated string exchanges = 2; } @@ -44,4 +46,5 @@ message ExchangeRateUpdate { repeated Candlestick sticks = 2; } repeated Candlesticks candlesticks = 9; + string currencyPair = 10; } diff --git a/exchanges/ratesproto/dcrrates_grpc.pb.go b/exchanges/ratesproto/dcrrates_grpc.pb.go new file mode 100644 index 000000000..495cdadd3 --- /dev/null +++ b/exchanges/ratesproto/dcrrates_grpc.pb.go @@ -0,0 +1,130 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.27.3 +// source: dcrrates.proto + +package __ + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DCRRates_SubscribeExchanges_FullMethodName = "/dcrrates.DCRRates/SubscribeExchanges" +) + +// DCRRatesClient is the client API for DCRRates service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// DCRRates takes a subscription from a client and pushes data as its received +// from external sources. +type DCRRatesClient interface { + SubscribeExchanges(ctx context.Context, in *ExchangeSubscription, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExchangeRateUpdate], error) +} + +type dCRRatesClient struct { + cc grpc.ClientConnInterface +} + +func NewDCRRatesClient(cc grpc.ClientConnInterface) DCRRatesClient { + return &dCRRatesClient{cc} +} + +func (c *dCRRatesClient) SubscribeExchanges(ctx context.Context, in *ExchangeSubscription, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExchangeRateUpdate], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &DCRRates_ServiceDesc.Streams[0], DCRRates_SubscribeExchanges_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ExchangeSubscription, ExchangeRateUpdate]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DCRRates_SubscribeExchangesClient = grpc.ServerStreamingClient[ExchangeRateUpdate] + +// DCRRatesServer is the server API for DCRRates service. +// All implementations must embed UnimplementedDCRRatesServer +// for forward compatibility. +// +// DCRRates takes a subscription from a client and pushes data as its received +// from external sources. +type DCRRatesServer interface { + SubscribeExchanges(*ExchangeSubscription, grpc.ServerStreamingServer[ExchangeRateUpdate]) error + mustEmbedUnimplementedDCRRatesServer() +} + +// UnimplementedDCRRatesServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDCRRatesServer struct{} + +func (UnimplementedDCRRatesServer) SubscribeExchanges(*ExchangeSubscription, grpc.ServerStreamingServer[ExchangeRateUpdate]) error { + return status.Errorf(codes.Unimplemented, "method SubscribeExchanges not implemented") +} +func (UnimplementedDCRRatesServer) mustEmbedUnimplementedDCRRatesServer() {} +func (UnimplementedDCRRatesServer) testEmbeddedByValue() {} + +// UnsafeDCRRatesServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DCRRatesServer will +// result in compilation errors. +type UnsafeDCRRatesServer interface { + mustEmbedUnimplementedDCRRatesServer() +} + +func RegisterDCRRatesServer(s grpc.ServiceRegistrar, srv DCRRatesServer) { + // If the following call pancis, it indicates UnimplementedDCRRatesServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DCRRates_ServiceDesc, srv) +} + +func _DCRRates_SubscribeExchanges_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(ExchangeSubscription) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(DCRRatesServer).SubscribeExchanges(m, &grpc.GenericServerStream[ExchangeSubscription, ExchangeRateUpdate]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DCRRates_SubscribeExchangesServer = grpc.ServerStreamingServer[ExchangeRateUpdate] + +// DCRRates_ServiceDesc is the grpc.ServiceDesc for DCRRates service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DCRRates_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "dcrrates.DCRRates", + HandlerType: (*DCRRatesServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "SubscribeExchanges", + Handler: _DCRRates_SubscribeExchanges_Handler, + ServerStreams: true, + }, + }, + Metadata: "dcrrates.proto", +} diff --git a/exchanges/ratesproto/runprotoc.sh b/exchanges/ratesproto/runprotoc.sh index c651c728d..b243eb880 100755 --- a/exchanges/ratesproto/runprotoc.sh +++ b/exchanges/ratesproto/runprotoc.sh @@ -1,3 +1,4 @@ -# Requires grpc and protoc-gen-go -# https://grpc.io/docs/quickstart/go.html#install-grpc -protoc dcrrates.proto --go_out=plugins=grpc:. +# Requires protoc, grpc and protoc-gen-go +# To install protoc: https://grpc.io/docs/protoc-installation +# To install grpc and protoc-gen-go: https://grpc.io/docs/quickstart/go.html#install-grpc + protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative dcrrates.proto