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 */ -}}
-
1 DCR =
- {{if eq $botState.BtcIndex "USD"}} - $ - {{end}} - {{printf "%.2f" $botState.Price}} {{$botState.BtcIndex}} +
+ 1 DCR = + {{if eq $botState.Index "USD"}} + $ + {{end}} + {{printf "%.2f" $botState.Price}} + {{$botState.Index}} +
@@ -34,13 +37,17 @@ DCR Vol. - BTC + Price - {{$botState.BtcIndex}} + {{$botState.Index}} {{range $botState.VolumeOrderedExchanges}} - - {{xcDisplayName .Token}} + + + + {{xcDisplayName .Token}} + ({{.CurrencyPair.QuoteAsset}}) + {{threeSigFigs .State.Volume}} @@ -57,11 +64,11 @@ {{end}} - {{printf "%.2f" ($botState.BtcToFiat .State.Price)}} + {{printf "%.2f" ($botState.PriceToFiat .State.Price .CurrencyPair)}} {{end}} - + Aggregate {{threeSigFigs $botState.Volume}} @@ -83,13 +90,13 @@
Bitcoin Indices
- {{range $token, $state := $botState.FiatIndices}} + {{range $token, $state := $botState.BitcoinIndices}}
{{toTitleCase $token}}
- {{if eq $botState.BtcIndex "USD"}} + {{if eq $botState.Index "USD"}} $ {{end}} - {{commaWithDecimal $state.Price 2}} {{$botState.BtcIndex}}
+ {{commaWithDecimal $state.Price 2}} {{$botState.Index}}
{{if eq $token "coindesk"}} Powered by CoinDesk {{end}} @@ -102,7 +109,7 @@ {{- /* RIGHT COLUMN */ -}}
-
+
@@ -120,7 +127,7 @@ > {{range $botState.VolumeOrderedExchanges}} {{end}} -
@@ -178,23 +179,13 @@
- - -
- - {{- /* AGGREGATE DEPTH STACKING */ -}} -
- - - + +
{{- /* ZOOM */ -}} @@ -222,7 +213,7 @@ {{- /* CHART */ -}} -
+
diff --git a/exchanges/bot.go b/exchanges/bot.go index 9b4621f7b..39afcbc70 100644 --- a/exchanges/bot.go +++ b/exchanges/bot.go @@ -31,8 +31,7 @@ const ( defaultDCRRatesPort = "7778" - aggregatedOrderbookKey = "aggregated" - orderbookKey = "depth" + orderbookKey = "depth" ) // ExchangeBotConfig is the configuration options for ExchangeBot. @@ -43,7 +42,7 @@ type ExchangeBotConfig struct { Disabled []string DataExpiry string RequestExpiry string - BtcIndex string + Index string Indent bool MasterBot string MasterCertFile string @@ -54,17 +53,20 @@ type ExchangeBotConfig struct { // structures are prepared. Make ExchangeBot with NewExchangeBot. type ExchangeBot struct { mtx sync.RWMutex - DcrBtcExchanges map[string]Exchange + DcrExchanges map[string]Exchange IndexExchanges map[string]Exchange Exchanges map[string]Exchange versionedCharts map[string]*versionedChart chartVersions map[string]int - // BtcIndex is the (typically fiat) currency to which the DCR price should be + // Index is the (typically fiat) currency to which the DCR price should be // converted by default. Other conversions are available via a lookup in // indexMap, but with slightly lower performance. // 3-letter currency code, e.g. USD. - BtcIndex string - indexMap map[string]FiatIndices + Index string + // indexMap is a map of exchanges to supported indices for valid currencies + // like BTC and USDT or any other asset that is added for dcr in the future. + // New currency pairs must have at least one entry. + indexMap map[string]map[CurrencyPair]FiatIndices currentState ExchangeBotState // Both currentState and stateCopy hold the same information. currentState // is updated by ExchangeBot, and a copy stored in stateCopy. After creation, @@ -96,36 +98,60 @@ type ExchangeBot struct { // ExchangeBotState is the current known state of all exchanges, in a certain // base currency, and a volume-averaged price and total volume in DCR. type ExchangeBotState struct { - BtcIndex string `json:"btc_index"` - BtcPrice float64 `json:"btc_fiat_price"` - Price float64 `json:"price"` - Volume float64 `json:"volume"` - DcrBtc map[string]*ExchangeState `json:"dcr_btc_exchanges"` + Index string `json:"index"` + BtcPrice float64 `json:"btc_fiat_price"` + Price float64 `json:"price"` + Volume float64 `json:"volume"` + DCRExchanges map[string]map[CurrencyPair]*ExchangeState `json:"dcr_exchanges"` // FiatIndices: // TODO: We only really need the BaseState for the fiat indices. - FiatIndices map[string]*ExchangeState `json:"btc_indices"` + FiatIndices map[string]map[CurrencyPair]*ExchangeState `json:"indices"` } // Copy an ExchangeState map. -func copyStates(m map[string]*ExchangeState) map[string]*ExchangeState { - c := make(map[string]*ExchangeState) - for k, v := range m { - c[k] = v +func copyStates(m map[string]map[CurrencyPair]*ExchangeState) map[string]map[CurrencyPair]*ExchangeState { + c := make(map[string]map[CurrencyPair]*ExchangeState) + for t, v := range m { + mc := make(map[CurrencyPair]*ExchangeState) + for p, s := range v { + mc[p] = s + } + c[t] = mc } return c } // Creates a pointer to a copy of the ExchangeBotState. func (state ExchangeBotState) copy() *ExchangeBotState { - state.DcrBtc = copyStates(state.DcrBtc) + state.DCRExchanges = copyStates(state.DCRExchanges) state.FiatIndices = copyStates(state.FiatIndices) return &state } -// BtcToFiat converts an amount of Bitcoin to fiat using the current calculated -// exchange rate. -func (state *ExchangeBotState) BtcToFiat(btc float64) float64 { - return state.BtcPrice * btc +// BtcToFiat converts an amount of {Bitcoin, USDT} to fiat using the current +// calculated exchange rate. +func (state *ExchangeBotState) PriceToFiat(price float64, currencyPair CurrencyPair) float64 { + switch currencyPair { + case CurrencyPairDCRBTC: + return state.BtcPrice * price + + case CurrencyPairDCRUSDT: + var usdtPrice, nSources float64 + for _, currencyIndices := range state.FiatIndices { + state := currencyIndices[USDTIndex] + if state != nil { + usdtPrice += state.Price + nSources++ + } + } + if usdtPrice != 0 { + usdtPrice = usdtPrice / nSources + } + return usdtPrice * price + + default: + return 0 + } } // FiatToBtc converts an amount of fiat in the default index to a value in BTC. @@ -136,22 +162,74 @@ func (state *ExchangeBotState) FiatToBtc(fiat float64) float64 { return fiat / state.BtcPrice } +// BitcoinIndices returns a map of all exchanges that provide a bitcoin index. +func (state *ExchangeBotState) BitcoinIndices() map[string]BaseState { + fiatIndices := make(map[string]BaseState) + for token, states := range state.FiatIndices { + s := states[BTCIndex] + if s != nil { + fiatIndices[token] = s.BaseState + } + } + return fiatIndices +} + +// Indices returns a map of known indices to their current fiat price. Returns +// an empty json object ({}) if it encounters an error. +func (state *ExchangeBotState) Indices() string { + sumIndexPrice := func(index CurrencyPair) float64 { + var price, nSource float64 + for _, states := range state.FiatIndices { + s := states[index] + if s != nil && s.Price > 0 { + price += s.Price + nSource++ + } + } + if price == 0 { + return 0 + } + return price / nSource + } + + fiatIndices := make(map[string]float64) + for _, states := range state.FiatIndices { + for i := range states { + if _, found := fiatIndices[i.String()]; !found { + fiatIndices[i.String()] = sumIndexPrice(i) + } + } + } + + b, err := json.Marshal(fiatIndices) + if err != nil { + log.Errorf("ExchangeBotState.Indices: json.Marshal error: %v", err) + return "{}" + } + + return string(b) +} + // ExchangeState doesn't have a Token field, so if the states are returned as a // slice (rather than ranging over a map), a token is needed. type tokenedExchange struct { Token string + CurrencyPair State *ExchangeState } // VolumeOrderedExchanges returns a list of tokenedExchange sorted by volume, // highest volume first. func (state *ExchangeBotState) VolumeOrderedExchanges() []*tokenedExchange { - xcList := make([]*tokenedExchange, 0, len(state.DcrBtc)) - for token, state := range state.DcrBtc { - xcList = append(xcList, &tokenedExchange{ - Token: token, - State: state, - }) + var xcList []*tokenedExchange + for token, states := range state.DCRExchanges { + for pair, state := range states { + xcList = append(xcList, &tokenedExchange{ + Token: token, + CurrencyPair: pair, + State: state, + }) + } } sort.Slice(xcList, func(i, j int) bool { return xcList[i].State.Volume > xcList[j].State.Volume @@ -159,46 +237,15 @@ func (state *ExchangeBotState) VolumeOrderedExchanges() []*tokenedExchange { return xcList } -// A price bin for the aggregated orderbook. The Volumes array will be length -// N = number of depth-reporting exchanges. If any exchange has an order book -// entry at price Price, then an agBookPt should be created. If a different -// exchange does not have an order at Price, there will be a 0 in Volumes at -// the exchange's index. An exchange's index in Volumes is set by its index -// in (aggregateOrderbook).Tokens. -type agBookPt struct { - Price float64 `json:"price"` - Volumes []float64 `json:"volumes"` -} - -// The aggregated depth data. Similar to DepthData, but with agBookPts instead. -// For aggregateData, the Time will indicate the most recent time at which an -// exchange with non-nil DepthData was updated. -type aggregateData struct { - Time int64 `json:"time"` - Bids []agBookPt `json:"bids"` - Asks []agBookPt `json:"asks"` -} - -// An aggregated orderbook. Combines all data from the DepthData of each -// Exchange. For aggregatedOrderbook, the Expiration is set to the time of the -// most recent DepthData update plus an additional (ExchangeBot).RequestExpiry, -// though new data may be available before then. -type aggregateOrderbook struct { - BtcIndex string `json:"btc_index"` - Price float64 `json:"price"` - Tokens []string `json:"tokens"` - UpdateTimes []int64 `json:"update_times"` - Data aggregateData `json:"data"` - Expiration int64 `json:"expiration"` -} - -// FiatIndices maps currency codes to Bitcoin exchange rates. +// FiatIndices maps currency codes to an asset's exchange rates, e.g +// Bitcoin-USD etc. type FiatIndices map[string]float64 // IndexUpdate is sent from the Exchange to the ExchangeBot indexChan when new // data is received. type IndexUpdate struct { - Token string + Token string + CurrencyPair Indices FiatIndices } @@ -219,7 +266,7 @@ type UpdateChannels struct { // The chart data structures that are encoded and cached are the // candlestickResponse and the depthResponse. type candlestickResponse struct { - BtcIndex string `json:"index"` + Index string `json:"index"` Price float64 `json:"price"` Sticks Candlesticks `json:"sticks"` Expiration int64 `json:"expiration"` @@ -267,24 +314,24 @@ func NewExchangeBot(config *ExchangeBotConfig) (*ExchangeBot, error) { if dataExpiry < time.Minute { return nil, fmt.Errorf("Expiration must be at least one minute") } - if config.BtcIndex == "" { - config.BtcIndex = DefaultCurrency + if config.Index == "" { + config.Index = DefaultCurrency } bot := &ExchangeBot{ - DcrBtcExchanges: make(map[string]Exchange), + DcrExchanges: make(map[string]Exchange), IndexExchanges: make(map[string]Exchange), Exchanges: make(map[string]Exchange), versionedCharts: make(map[string]*versionedChart), chartVersions: make(map[string]int), - BtcIndex: config.BtcIndex, - indexMap: make(map[string]FiatIndices), + Index: config.Index, + indexMap: make(map[string]map[CurrencyPair]FiatIndices), currentState: ExchangeBotState{ - BtcIndex: config.BtcIndex, - Price: 0, - Volume: 0, - DcrBtc: make(map[string]*ExchangeState), - FiatIndices: make(map[string]*ExchangeState), + Index: config.Index, + Price: 0, + Volume: 0, + DCRExchanges: make(map[string]map[CurrencyPair]*ExchangeState), + FiatIndices: make(map[string]map[CurrencyPair]*ExchangeState), }, currentStateBytes: []byte{}, DataExpiry: dataExpiry, @@ -353,20 +400,20 @@ func NewExchangeBot(config *ExchangeBotConfig) (*ExchangeBot, error) { bot.Exchanges[token] = xc } - for token, constructor := range BtcIndices { + for token, constructor := range Indices { buildExchange(token, constructor, bot.IndexExchanges) } for token, constructor := range DcrExchanges { - buildExchange(token, constructor, bot.DcrBtcExchanges) + buildExchange(token, constructor, bot.DcrExchanges) } - if len(bot.DcrBtcExchanges) == 0 { - return nil, fmt.Errorf("no DCR-BTC exchanges were initialized") + if len(bot.DcrExchanges) == 0 { + return nil, fmt.Errorf("no DCR exchanges were initialized") } if len(bot.IndexExchanges) == 0 { - return nil, fmt.Errorf("no BTC-fiat exchanges were initialized") + return nil, fmt.Errorf("no {BTC, USDT}-fiat exchanges were initialized") } return bot, nil @@ -422,13 +469,22 @@ func (bot *ExchangeBot) Start(ctx context.Context, wg *sync.WaitGroup) { reconnectionAttempt = 0 continue } - // Send the update through the Exchange so that appropriate attributes - // are set. + // Send the update through the Exchange so that appropriate + // attributes are set. if IsDcrExchange(update.Token) { - state := exchangeStateFromProto(update) - bot.Exchanges[update.Token].Update(state) - } else if IsBtcIndex(update.Token) { - bot.Exchanges[update.Token].UpdateIndices(update.GetIndices()) + currencyPair, state := exchangeStateFromProto(update) + if !currencyPair.IsValidDCRPair() { + log.Errorf("Received update for unknown currency pair %s", currencyPair) + } else { + bot.Exchanges[update.Token].Update(currencyPair, state) + } + } else if IsIndex(update.Token) { + currencyIndex := CurrencyPair(update.GetCurrencyPair()) + if !currencyIndex.IsValidIndex() { + log.Errorf("Received update for unknown index %s", currencyIndex) + } else { + bot.Exchanges[update.Token].UpdateIndices(currencyIndex, update.GetIndices()) + } } } }() @@ -454,7 +510,7 @@ out: for { select { case update := <-bot.exchangeChan: - log.Tracef("exchange update received from %s with a BTC price %f, ", update.Token, update.State.Price) + log.Tracef("exchange update received from %s (Currency Pair: %s) with price %f, ", update.Token, update.CurrencyPair, update.State.Price) err := bot.updateExchange(update) if err != nil { log.Warnf("Error encountered in exchange update: %v", err) @@ -462,9 +518,9 @@ out: } bot.signalExchangeUpdate(update) case update := <-bot.indexChan: - btcPrice, found := update.Indices[bot.BtcIndex] + price, found := update.Indices[bot.Index] if found { - log.Tracef("index update received from %s with %d indices, %s price for Bitcoin is %f", update.Token, len(update.Indices), bot.BtcIndex, btcPrice) + log.Tracef("index update received from %s with %d indices, %s price for %s is %f", update.Token, len(update.Indices), bot.Index, update.CurrencyPair, price) } err := bot.updateIndices(update) if err != nil { @@ -515,7 +571,7 @@ func (bot *ExchangeBot) connectMasterBot(ctx context.Context, delay time.Duratio bot.masterConnection = conn grpcClient := dcrrates.NewDCRRatesClient(conn) stream, err := grpcClient.SubscribeExchanges(ctx, &dcrrates.ExchangeSubscription{ - BtcIndex: bot.BtcIndex, + Index: bot.Index, Exchanges: bot.subscribedExchanges(), }) if err != nil { @@ -583,34 +639,42 @@ func (bot *ExchangeBot) State() *ExchangeBotState { return bot.stateCopy } -// ConvertedState returns an ExchangeBotState with a base of the provided -// currency code, if available. -func (bot *ExchangeBot) ConvertedState(code string) (*ExchangeBotState, error) { - bot.mtx.RLock() - defer bot.mtx.RUnlock() - fiatIndices := make(map[string]*ExchangeState) +// indicesForCode must be called under bot.mtx lock. +func (bot *ExchangeBot) indicesForCode(code string) map[string]map[CurrencyPair]*ExchangeState { + fiatIndices := make(map[string]map[CurrencyPair]*ExchangeState) for token, indices := range bot.indexMap { - for symbol, price := range indices { - if symbol == code { - fiatIndices[token] = &ExchangeState{BaseState: BaseState{Price: price}} + for currencyPair, indice := range indices { + for symbol, price := range indice { + if symbol == code { + fiatIndices[token] = map[CurrencyPair]*ExchangeState{ + currencyPair: {BaseState: BaseState{Price: price}}, + } + } } } } + return fiatIndices +} - dcrPrice, volume := bot.processState(bot.currentState.DcrBtc, true) - btcPrice, _ := bot.processState(fiatIndices, false) +// ConvertedState returns an ExchangeBotState with a base of the provided +// currency code, if available. +func (bot *ExchangeBot) ConvertedState(code string) (*ExchangeBotState, error) { + bot.mtx.RLock() + defer bot.mtx.RUnlock() + dcrPrice, volume := bot.dcrPriceAndVolume(code) + btcPrice := bot.indexPrice(BTCIndex, code) if dcrPrice == 0 || btcPrice == 0 { bot.failed = true return nil, fmt.Errorf("Unable to process price for currency %s", code) } state := ExchangeBotState{ - BtcIndex: code, - Volume: volume * btcPrice, - Price: dcrPrice * btcPrice, - BtcPrice: btcPrice, - DcrBtc: bot.currentState.DcrBtc, - FiatIndices: fiatIndices, + Index: code, + Volume: volume, + Price: dcrPrice, + BtcPrice: dcrPrice, + DCRExchanges: bot.currentState.DCRExchanges, + FiatIndices: bot.indicesForCode(code), } return state.copy(), nil @@ -618,10 +682,10 @@ func (bot *ExchangeBot) ConvertedState(code string) (*ExchangeBotState, error) { // ExchangeRates is the dcr and btc prices converted to fiat. type ExchangeRates struct { - BtcIndex string `json:"btcIndex"` - DcrPrice float64 `json:"dcrPrice"` - BtcPrice float64 `json:"btcPrice"` - Exchanges map[string]BaseState `json:"exchanges"` + Index string `json:"index"` + DcrPrice float64 `json:"dcrPrice"` + BtcPrice float64 `json:"btcPrice"` + Exchanges map[string]map[CurrencyPair]BaseState `json:"exchanges"` } // Rates is the current exchange rates for dcr and btc. @@ -630,16 +694,20 @@ func (bot *ExchangeBot) Rates() *ExchangeRates { defer bot.mtx.RUnlock() s := bot.stateCopy - xcs := make(map[string]BaseState, len(s.DcrBtc)) - for token, xcState := range s.DcrBtc { - xcs[token] = xcState.BaseState + xcMarkets := make(map[string]map[CurrencyPair]BaseState, len(s.DCRExchanges)) + for token, xcStates := range s.DCRExchanges { + xcs := make(map[CurrencyPair]BaseState, len(xcStates)) + for currencyPair, xcState := range xcStates { + xcs[currencyPair] = xcState.BaseState + } + xcMarkets[token] = xcs } return &ExchangeRates{ - BtcIndex: s.BtcIndex, + Index: s.Index, DcrPrice: s.Price, BtcPrice: s.BtcPrice, - Exchanges: xcs, + Exchanges: xcMarkets, } } @@ -648,25 +716,16 @@ func (bot *ExchangeBot) Rates() *ExchangeRates { func (bot *ExchangeBot) ConvertedRates(code string) (*ExchangeRates, error) { bot.mtx.RLock() defer bot.mtx.RUnlock() - fiatIndices := make(map[string]*ExchangeState) - for token, indices := range bot.indexMap { - for symbol, price := range indices { - if symbol == code { - fiatIndices[token] = &ExchangeState{BaseState: BaseState{Price: price}} - } - } - } - - dcrPrice, _ := bot.processState(bot.currentState.DcrBtc, true) - btcPrice, _ := bot.processState(fiatIndices, false) - if dcrPrice == 0 || btcPrice == 0 { + dcrPrice, _ := bot.dcrPriceAndVolume(code) + btcPrice := bot.indexPrice(BTCIndex, code) + if btcPrice == 0 || dcrPrice == 0 { bot.failed = true return nil, fmt.Errorf("Unable to process price for currency %s", code) } return &ExchangeRates{ - BtcIndex: code, - DcrPrice: dcrPrice * btcPrice, + Index: code, + DcrPrice: dcrPrice, BtcPrice: btcPrice, }, nil } @@ -710,21 +769,23 @@ func (bot *ExchangeBot) AvailableIndices() []string { indices = append(indices, index) } for _, fiatIndices := range bot.indexMap { - for symbol := range fiatIndices { - add(symbol) + for _, indices := range fiatIndices { + for symbol := range indices { + add(symbol) + } } } sort.Sort(indices) return indices } -// Indices is the fiat indices for a given BTC index exchange. -func (bot *ExchangeBot) Indices(token string) FiatIndices { +// Indices is the fiat indices for a given {BTC, USDT} index exchange. +func (bot *ExchangeBot) Indices(token string) map[CurrencyPair]FiatIndices { bot.mtx.RLock() defer bot.mtx.RUnlock() - indices := make(FiatIndices) - for code, price := range bot.indexMap[token] { - indices[code] = price + indices := make(map[CurrencyPair]FiatIndices) + for currencyIndex, indice := range bot.indexMap[token] { + indices[currencyIndex] = indice } return indices } @@ -746,60 +807,99 @@ func (bot *ExchangeBot) cachedChartVersion(chartId string) int { return cid } -// processState is a helper function to process a slice of ExchangeState into -// a price, and optionally a volume sum, and perform some cleanup along the way. +// processState is a helper function to process a slice of ExchangeState into a +// price, and optionally a volume sum, and perform some cleanup along the way. // If volumeAveraged is false, all exchanges are given equal weight in the avg. -func (bot *ExchangeBot) processState(states map[string]*ExchangeState, volumeAveraged bool) (float64, float64) { - var priceAccumulator, volSum float64 - var deletions []string +// If exchange is invalid, a bool false is returned as a last return value. +func (bot *ExchangeBot) processState(token, code string, states map[CurrencyPair]*ExchangeState, volumeAveraged bool) (float64, float64, bool) { oldestValid := time.Now().Add(-bot.RequestExpiry) - for token, state := range states { - if bot.Exchanges[token].LastUpdate().Before(oldestValid) { - deletions = append(deletions, token) - continue - } + if bot.Exchanges[token].LastUpdate().Before(oldestValid) { + return 0, 0, false + } + + var priceAccumulator, volSum float64 + for currencyPair, state := range states { volume := 1.0 if volumeAveraged { volume = state.Volume } volSum += volume - priceAccumulator += volume * state.Price - } - for _, token := range deletions { - delete(states, token) + + // Convert price to bot.Index. + price := state.Price + switch currencyPair { + case CurrencyPairDCRBTC: + price = bot.indexPrice(BTCIndex, code) * price + case CurrencyPairDCRUSDT: + price = bot.indexPrice(USDTIndex, code) * price + } + if price == 0 { // missing index price for currencyPair. + return 0, 0, false + } + + priceAccumulator += volume * price } + if volSum == 0 { - return 0, 0 + return 0, 0, true } - return priceAccumulator / volSum, volSum + return priceAccumulator / volSum, volSum, true } -// updateExchange processes an update from a Decred-BTC Exchange. +// indexPrice retrieves the index price for the provided currency index +// {BTC-Index, USDT-Index}. Must be called under bot.mutex lock. +func (bot *ExchangeBot) indexPrice(index CurrencyPair, code string) float64 { + var price, nSource float64 + for _, currencyIndex := range bot.indexMap { + indices := currencyIndex[index] + if len(indices) != 0 && indices[code] > 0 { + price += indices[code] + nSource++ + } + } + if price == 0 { + return 0 + } + return price / nSource +} + +// updateExchange processes an update from a Decred-{Asset} Exchange. func (bot *ExchangeBot) updateExchange(update *ExchangeUpdate) error { bot.mtx.Lock() defer bot.mtx.Unlock() if update.State.Candlesticks != nil { for bin := range update.State.Candlesticks { - bot.incrementChart(genCacheID(update.Token, string(bin))) + bot.incrementChart(genCacheID(update.CurrencyPair.String(), update.Token, string(bin))) } } if update.State.Depth != nil { - bot.incrementChart(genCacheID(update.Token, orderbookKey)) - bot.incrementChart(genCacheID(aggregatedOrderbookKey, orderbookKey)) + bot.incrementChart(genCacheID(update.CurrencyPair.String(), update.Token, orderbookKey)) + } + + if bot.currentState.DCRExchanges[update.Token] == nil { + bot.currentState.DCRExchanges[update.Token] = make(map[CurrencyPair]*ExchangeState) } - bot.currentState.DcrBtc[update.Token] = update.State + bot.currentState.DCRExchanges[update.Token][update.CurrencyPair] = update.State return bot.updateState() } -// updateIndices processes an update from an Bitcoin index source, essentially -// a map pairing currency codes to bitcoin prices. +// updateIndices processes an update from an {Bitcoin, USDT} index source, +// essentially a map pairing currency codes to bitcoin or usdt prices. func (bot *ExchangeBot) updateIndices(update *IndexUpdate) error { bot.mtx.Lock() defer bot.mtx.Unlock() - bot.indexMap[update.Token] = update.Indices - price, hasCode := update.Indices[bot.config.BtcIndex] + if bot.indexMap[update.Token] == nil { + bot.indexMap[update.Token] = make(map[CurrencyPair]FiatIndices) + } + + bot.indexMap[update.Token][update.CurrencyPair] = update.Indices + price, hasCode := update.Indices[bot.config.Index] if hasCode { - bot.currentState.FiatIndices[update.Token] = &ExchangeState{ + if bot.currentState.FiatIndices[update.Token] == nil { + bot.currentState.FiatIndices[update.Token] = make(map[CurrencyPair]*ExchangeState) + } + + bot.currentState.FiatIndices[update.Token][update.CurrencyPair] = &ExchangeState{ BaseState: BaseState{ Price: price, Stamp: time.Now().Unix(), @@ -807,19 +907,44 @@ func (bot *ExchangeBot) updateIndices(update *IndexUpdate) error { } return bot.updateState() } - log.Warnf("Default currency code, %s, not contained in update from %s", bot.BtcIndex, update.Token) + log.Warnf("Default currency code, %s, not contained in update from %s", bot.Index, update.Token) return nil } +// dcrPriceAndVolume calculates and returns dcr price and volume. The returned +// dcr price is converted to the provided index code. Must be called under +// bot.mtx lock. +func (bot *ExchangeBot) dcrPriceAndVolume(code string) (float64, float64) { + var dcrPrice, volume, nSources float64 + for token, xcStates := range bot.currentState.DCRExchanges { + processedDcrPrice, processedVolume, ok := bot.processState(token, code, xcStates, true) + if !ok { + continue + } + + volume += processedVolume + if processedDcrPrice != 0 { + dcrPrice += processedDcrPrice + nSources++ + } + } + + if dcrPrice == 0 { + return 0, 0 + } + + return dcrPrice / nSources, volume +} + // Called from both updateIndices and updateExchange (under mutex lock). func (bot *ExchangeBot) updateState() error { - dcrPrice, volume := bot.processState(bot.currentState.DcrBtc, true) - btcPrice, _ := bot.processState(bot.currentState.FiatIndices, false) - if dcrPrice == 0 || btcPrice == 0 { + btcPrice := bot.indexPrice(BTCIndex, bot.Index) + dcrPrice, volume := bot.dcrPriceAndVolume(bot.Index) + if btcPrice == 0 || dcrPrice == 0 { bot.failed = true } else { bot.failed = false - bot.currentState.Price = dcrPrice * btcPrice + bot.currentState.Price = dcrPrice bot.currentState.BtcPrice = btcPrice bot.currentState.Volume = volume } @@ -880,7 +1005,7 @@ func (bot *ExchangeBot) Cycle() { } } -// Price gets the lastest Price in the default currency (BtcIndex). +// Price gets the latest Price in the default currency (Index). func (bot *ExchangeBot) Price() float64 { bot.mtx.RLock() defer bot.mtx.RUnlock() @@ -915,7 +1040,7 @@ func (bot *ExchangeBot) Conversion(dcrVal float64) *Conversion { if xcState != nil { return &Conversion{ Value: xcState.Price * dcrVal, - Index: xcState.BtcIndex, + Index: xcState.Index, } } // Haven't gotten data yet, but we're running. @@ -939,8 +1064,12 @@ func (bot *ExchangeBot) fetchFromCache(chartID string) (data []byte, bestVersion // QuickSticks returns the up-to-date candlestick data for the specified // exchange and bin width, pulling from the cache if appropriate. -func (bot *ExchangeBot) QuickSticks(token string, rawBin string) ([]byte, error) { - chartID := genCacheID(token, rawBin) +func (bot *ExchangeBot) QuickSticks(token string, market CurrencyPair, rawBin string) ([]byte, error) { + if !market.IsValidDCRPair() { + return nil, fmt.Errorf("invalid market %s", market) + } + + chartID := genCacheID(market.String(), token, rawBin) bin := candlestickKey(rawBin) data, bestVersion, isGood := bot.fetchFromCache(chartID) if isGood { @@ -951,10 +1080,15 @@ func (bot *ExchangeBot) QuickSticks(token string, rawBin string) ([]byte, error) bot.mtx.Lock() defer bot.mtx.Unlock() - state, found := bot.currentState.DcrBtc[token] + xcStates, found := bot.currentState.DCRExchanges[token] if !found { return nil, fmt.Errorf("Failed to find DCR exchange state for %s", token) } + + state, found := xcStates[market] + if !found { + return nil, fmt.Errorf("Failed to find DCR exchange state for %s (Currency Pair: %s)", token, market) + } if state.Candlesticks == nil { return nil, fmt.Errorf("Failed to find candlesticks for %s", token) } @@ -968,9 +1102,8 @@ func (bot *ExchangeBot) QuickSticks(token string, rawBin string) ([]byte, error) } expiration := sticks[len(sticks)-1].Start.Add(2 * bin.duration()) - chart, err := bot.encodeJSON(&candlestickResponse{ - BtcIndex: bot.BtcIndex, + Index: bot.Index, Price: bot.currentState.Price, Sticks: sticks, Expiration: expiration.Unix(), @@ -989,130 +1122,37 @@ func (bot *ExchangeBot) QuickSticks(token string, rawBin string) ([]byte, error) return vChart.chart, nil } -// Move the DepthPoint array into a map whose entries are agBookPt, inserting -// the (DepthPoint).Quantity values at xcIndex of Volumes. Creates Volumes -// if it does not yet exist. -func mapifyDepthPoints(source []DepthPoint, target map[int64]agBookPt, xcIndex, ptCount int) { - for _, pt := range source { - k := eightPtKey(pt.Price) - _, found := target[k] - if !found { - target[k] = agBookPt{ - Price: pt.Price, - Volumes: make([]float64, ptCount), - } - } - target[k].Volumes[xcIndex] = pt.Quantity +// QuickDepth returns the up-to-date depth chart data for the specified exchange +// market, pulling from the cache if appropriate. +func (bot *ExchangeBot) QuickDepth(token string, market CurrencyPair) (chart []byte, err error) { + if !market.IsValidDCRPair() { + return nil, fmt.Errorf("invalid market %s", market) } -} - -// A list of eightPtKey keys from an orderbook tracking map. Used for sorting. -func agBookMapKeys(book map[int64]agBookPt) []int64 { - keys := make([]int64, 0, len(book)) - for k := range book { - keys = append(keys, k) - } - return keys -} - -// After the aggregate orderbook map is fully assembled, sort the keys and -// process the map into a list of lists. -func unmapAgOrders(book map[int64]agBookPt, reverse bool) []agBookPt { - orderedBook := make([]agBookPt, 0, len(book)) - keys := agBookMapKeys(book) - if reverse { - sort.Slice(keys, func(i, j int) bool { return keys[j] < keys[i] }) - } else { - sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) - } - for _, k := range keys { - orderedBook = append(orderedBook, book[k]) - } - return orderedBook -} - -// Make an aggregate orderbook from all depth data. -func (bot *ExchangeBot) aggOrderbook() *aggregateOrderbook { - state := bot.State() - if state == nil { - return nil - } - bids := make(map[int64]agBookPt) - asks := make(map[int64]agBookPt) - - oldestUpdate := time.Now().Unix() - var newestTime int64 - // First, grab the tokens for exchanges with depth data so that they can be - // counted and sorted alphabetically. - tokens := []string{} - for token, xcState := range state.DcrBtc { - if !xcState.HasDepth() { - continue - } - tokens = append(tokens, token) - } - numXc := len(tokens) - updateTimes := make([]int64, 0, numXc) - sort.Strings(tokens) - for i, token := range tokens { - xcState := state.DcrBtc[token] - depth := xcState.Depth - if depth.Time < oldestUpdate { - oldestUpdate = depth.Time - } - if depth.Time > newestTime { - newestTime = depth.Time - } - updateTimes = append(updateTimes, depth.Time) - mapifyDepthPoints(depth.Bids, bids, i, numXc) - mapifyDepthPoints(depth.Asks, asks, i, numXc) - } - return &aggregateOrderbook{ - Tokens: tokens, - BtcIndex: bot.BtcIndex, - Price: state.Price, - UpdateTimes: updateTimes, - Data: aggregateData{ - Time: newestTime, - Bids: unmapAgOrders(bids, true), - Asks: unmapAgOrders(asks, false), - }, - Expiration: oldestUpdate + int64(bot.RequestExpiry.Seconds()), - } -} -// QuickDepth returns the up-to-date depth chart data for the specified -// exchange, pulling from the cache if appropriate. -func (bot *ExchangeBot) QuickDepth(token string) (chart []byte, err error) { - chartID := genCacheID(token, orderbookKey) + chartID := genCacheID(market.String(), token, orderbookKey) data, bestVersion, isGood := bot.fetchFromCache(chartID) if isGood { return data, nil } - if token == aggregatedOrderbookKey { - agDepth := bot.aggOrderbook() - if agDepth == nil { - return nil, fmt.Errorf("Failed to find depth for %s", token) - } - chart, err = bot.encodeJSON(agDepth) - } else { - bot.mtx.Lock() - defer bot.mtx.Unlock() - xcState, found := bot.currentState.DcrBtc[token] - if !found { - return nil, fmt.Errorf("Failed to find DCR exchange state for %s", token) - } - if xcState.Depth == nil { - return nil, fmt.Errorf("Failed to find depth for %s", token) - } - chart, err = bot.encodeJSON(&depthResponse{ - BtcIndex: bot.BtcIndex, - Price: bot.currentState.Price, - Data: xcState.Depth, - Expiration: xcState.Depth.Time + int64(bot.RequestExpiry.Seconds()), - }) + bot.mtx.Lock() + defer bot.mtx.Unlock() + xcStates, found := bot.currentState.DCRExchanges[token] + if !found { + return nil, fmt.Errorf("Failed to find DCR exchange state for %s (Currency Pair: %s)", token, market) + } + + state, ok := xcStates[market] + if !ok || state.Depth == nil { + return nil, fmt.Errorf("Failed to find depth for %s (Currency Pair: %s)", token, market) } + + chart, err = bot.encodeJSON(&depthResponse{ + BtcIndex: bot.Index, + Price: bot.currentState.Price, + Data: state.Depth, + Expiration: state.Depth.Time + int64(bot.RequestExpiry.Seconds()), + }) if err != nil { return nil, fmt.Errorf("JSON encode error for %s depth chart", token) } diff --git a/exchanges/exchanges.go b/exchanges/exchanges.go index 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