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 bfbbaf9e8..fe012544e 100644
--- a/cmd/dcrdata/internal/explorer/explorer.go
+++ b/cmd/dcrdata/internal/explorer/explorer.go
@@ -786,21 +786,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:
@@ -811,14 +818,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
@@ -846,3 +861,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 2cf68169c..f34a77403 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"
}
@@ -2549,9 +2549,7 @@ func (exp *explorerUI) StatsPage(w http.ResponseWriter, r *http.Request) {
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 bfc68f06a..024f6000c 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 c0f13c0dd..46b6d03c1 100644
--- a/cmd/dcrdata/views/extras.tmpl
+++ b/cmd/dcrdata/views/extras.tmpl
@@ -63,7 +63,7 @@
-
+
@@ -189,7 +189,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 */ -}}
Aggregate |
{{threeSigFigs $botState.Volume}}
@@ -83,13 +90,13 @@
- {{range $token, $state := $botState.FiatIndices}}
+ {{range $token, $state := $botState.BitcoinIndices}}
- {{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 c80a57b11..6600654e5 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)
}
}
@@ -2429,7 +2600,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,
@@ -2488,7 +2659,7 @@ func (dcr *DecredDEX) Refresh() {
}
}
- dcr.Update(&ExchangeState{
+ dcr.Update(CurrencyPairDCRBTC, &ExchangeState{
BaseState: BaseState{
Price: depth.MidGap(),
Change: change,
@@ -2850,7 +3021,7 @@ func (dcr *DecredDEX) setOrderBook(ob *msgjson.OrderBook) {
depth := dcr.wsDepthSnapshot()
- dcr.Update(&ExchangeState{
+ dcr.Update(CurrencyPairDCRBTC, &ExchangeState{
BaseState: BaseState{
Price: depth.MidGap(),
// Change: priceChange, // With candlesticks
@@ -2926,3 +3097,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
|