From 440fa3bacd0346e1d7d790733e5d0188e9add1a2 Mon Sep 17 00:00:00 2001 From: thuanle Date: Sat, 25 Apr 2026 00:15:22 +0700 Subject: [PATCH] Fix concurrent map read/write race condition in MarketData Add sync.RWMutex to protect future and spot price maps accessed by WebSocket handlers (writers) and Telegram bot handlers (readers). Also adds Alpha token support with caching. Co-Authored-By: Claude Opus 4.7 --- go.mod | 14 +- go.sum | 33 +++-- internal/data/imarket.go | 6 + internal/data/market/alpha_tokens.go | 179 +++++++++++++++++++++++ internal/data/market/future_price.go | 16 +- internal/data/market/main.go | 20 ++- internal/data/market/spot_price.go | 27 +++- internal/helper/binancex/symbol.go | 12 +- internal/services/tele/commands/token.go | 21 ++- 9 files changed, 297 insertions(+), 31 deletions(-) create mode 100644 internal/data/market/alpha_tokens.go diff --git a/go.mod b/go.mod index 2e8caff..62baa2e 100644 --- a/go.mod +++ b/go.mod @@ -9,21 +9,21 @@ require ( github.com/joho/godotenv v1.5.1 github.com/rs/zerolog v1.32.0 github.com/samber/lo v1.39.0 - golang.org/x/text v0.16.0 + golang.org/x/text v0.21.0 gopkg.in/telebot.v3 v3.2.1 ) require ( github.com/bitly/go-simplejson v0.5.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jpillora/backoff v1.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 6843da1..e104f0b 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/adshao/go-binance/v2 v2.5.0 h1:mk8ylSjIzDYVBF9Wf2KXu6GWD/Ws4LLzD9q2R2mqZB0= -github.com/adshao/go-binance/v2 v2.5.0/go.mod h1:41Up2dG4NfMXpCldrDPETEtiOq+pHoGsFZ73xGgaumo= +github.com/adshao/go-binance/v2 v2.8.5 h1:2i8uVFrt1HbZPggnfdL1A1g/PS9MeD1FnoBoIXNhbow= +github.com/adshao/go-binance/v2 v2.8.5/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -135,8 +135,8 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= -github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -155,6 +155,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -212,6 +213,8 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/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/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -220,8 +223,8 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= @@ -258,12 +261,12 @@ github.com/jedib0t/go-pretty/v6 v6.5.5 h1:PpIU8lOjxvVYGGKule0QxxJfNysUSbC9lggQU2 github.com/jedib0t/go-pretty/v6 v6.5.5/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -316,11 +319,9 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -368,6 +369,8 @@ github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5A github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -509,8 +512,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-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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= @@ -624,8 +627,8 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -637,8 +640,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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/data/imarket.go b/internal/data/imarket.go index d0fa699..ba7a6d4 100644 --- a/internal/data/imarket.go +++ b/internal/data/imarket.go @@ -1,9 +1,15 @@ package data +import "me.thuanle/bbot/internal/data/market" + type IMarket interface { GetFuturePrice(symbol string) (float64, float64, int64, bool) GetAllFundRate() (map[string]float64, map[string]int64) GetSpotPrice(symbol string) (float64, bool) GetMarginInterestRates() map[string]float64 + + // Alpha token methods + IsAlphaToken(symbol string) bool + GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool) } diff --git a/internal/data/market/alpha_tokens.go b/internal/data/market/alpha_tokens.go new file mode 100644 index 0000000..8bdb6ff --- /dev/null +++ b/internal/data/market/alpha_tokens.go @@ -0,0 +1,179 @@ +package market + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/rs/zerolog/log" +) + +// AlphaTokenInfo represents the response from Binance Alpha token API +type AlphaTokenInfo struct { + TokenID string `json:"tokenId"` + ChainID string `json:"chainId"` + ChainIconURL string `json:"chainIconUrl"` + ChainName string `json:"chainName"` + ContractAddress string `json:"contractAddress"` + Name string `json:"name"` + Symbol string `json:"symbol"` + IconURL string `json:"iconUrl"` + Price string `json:"price"` + PercentChange24h string `json:"percentChange24h"` + Volume24h string `json:"volume24h"` + MarketCap string `json:"marketCap"` + FDV string `json:"fdv"` + Liquidity string `json:"liquidity"` + TotalSupply string `json:"totalSupply"` + CirculatingSupply string `json:"circulatingSupply"` + Holders string `json:"holders"` + Decimals int `json:"decimals"` + ListingCex bool `json:"listingCex"` + HotTag bool `json:"hotTag"` + CexCoinName string `json:"cexCoinName"` + CanTransfer bool `json:"canTransfer"` + Denomination int `json:"denomination"` + Offline bool `json:"offline"` + TradeDecimal int `json:"tradeDecimal"` + AlphaID string `json:"alphaId"` + Offsell bool `json:"offsell"` + PriceHigh24h string `json:"priceHigh24h"` + PriceLow24h string `json:"priceLow24h"` + Count24h string `json:"count24h"` + OnlineTge bool `json:"onlineTge"` + OnlineAirdrop bool `json:"onlineAirdrop"` + Score int `json:"score"` + CexOffDisplay bool `json:"cexOffDisplay"` + StockState bool `json:"stockState"` + ListingTime int64 `json:"listingTime"` + MulPoint int `json:"mulPoint"` + BnExclusiveState bool `json:"bnExclusiveState"` +} + +// AlphaTokenResponse represents the API response structure +type AlphaTokenResponse struct { + Code string `json:"code"` + Message *string `json:"message"` + MessageDetail *string `json:"messageDetail"` + Data []AlphaTokenInfo `json:"data"` +} + +// GetPrice returns the price as float64 +func (a *AlphaTokenInfo) GetPrice() float64 { + price, err := strconv.ParseFloat(a.Price, 64) + if err != nil { + log.Error().Err(err).Str("symbol", a.Symbol).Msg("Failed to parse Alpha token price") + return 0 + } + return price +} + +// GetPercentChange24h returns the 24h percentage change as float64 +func (a *AlphaTokenInfo) GetPercentChange24h() float64 { + change, err := strconv.ParseFloat(a.PercentChange24h, 64) + if err != nil { + log.Error().Err(err).Str("symbol", a.Symbol).Msg("Failed to parse Alpha token 24h change") + return 0 + } + return change +} + +// fetchAlphaTokens fetches Alpha tokens from Binance API +func (ms *MarketData) fetchAlphaTokens() ([]AlphaTokenInfo, error) { + const alphaTokenURL = "https://www.binance.com/bapi/defi/v1/public/wallet-direct/buw/wallet/cex/alpha/all/token/list" + + client := &http.Client{ + Timeout: 10 * time.Second, + } + + resp, err := client.Get(alphaTokenURL) + if err != nil { + return nil, fmt.Errorf("failed to fetch Alpha tokens: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Alpha token API returned status: %d", resp.StatusCode) + } + + var tokenResponse AlphaTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil { + return nil, fmt.Errorf("failed to decode Alpha token response: %w", err) + } + + if tokenResponse.Code != "000000" { + return nil, fmt.Errorf("Alpha token API returned error code: %s", tokenResponse.Code) + } + + return tokenResponse.Data, nil +} + +// refreshAlphaTokenCache refreshes the Alpha token cache +func (ms *MarketData) refreshAlphaTokenCache() { + ms.alphaCacheMutex.Lock() + defer ms.alphaCacheMutex.Unlock() + + log.Info().Msg("Refreshing Alpha token cache") + + tokens, err := ms.fetchAlphaTokens() + if err != nil { + log.Error().Err(err).Msg("Failed to refresh Alpha token cache") + return + } + + // Clear existing cache + ms.alphaTokens = make(map[string]AlphaTokenInfo) + + // Populate cache with new data + for _, token := range tokens { + symbol := token.Symbol // Already uppercase from API + ms.alphaTokens[symbol] = token + } + + // Update spot prices for Alpha tokens (separate lock to avoid holding both mutexes) + ms.mu.Lock() + for _, token := range tokens { + symbol := token.Symbol + if price := token.GetPrice(); price > 0 { + ms.spotPrice[symbol] = price + } + } + ms.mu.Unlock() + + ms.lastAlphaCacheUpdate = time.Now() + log.Info().Int("count", len(tokens)).Msg("Alpha token cache refreshed successfully") +} + +// GetAlphaToken returns Alpha token info by symbol +func (ms *MarketData) GetAlphaToken(symbol string) (AlphaTokenInfo, bool) { + ms.alphaCacheMutex.RLock() + defer ms.alphaCacheMutex.RUnlock() + + token, exists := ms.alphaTokens[symbol] + return token, exists +} + +// IsAlphaToken checks if a symbol is an Alpha token +func (ms *MarketData) IsAlphaToken(symbol string) bool { + _, exists := ms.GetAlphaToken(symbol) + return exists +} + +// shouldRefreshAlphaCache checks if the Alpha cache should be refreshed +func (ms *MarketData) shouldRefreshAlphaCache() bool { + // Refresh if cache is empty (first time) or if it's older than 30 minutes + if len(ms.alphaTokens) == 0 { + return true + } + + return time.Since(ms.lastAlphaCacheUpdate) > 30*time.Minute +} + +// ensureAlphaCacheLoaded ensures Alpha cache is loaded, refreshes if needed +func (ms *MarketData) ensureAlphaCacheLoaded() { + if ms.shouldRefreshAlphaCache() { + ms.refreshAlphaTokenCache() + } +} \ No newline at end of file diff --git a/internal/data/market/future_price.go b/internal/data/market/future_price.go index 67aec61..872a80e 100644 --- a/internal/data/market/future_price.go +++ b/internal/data/market/future_price.go @@ -7,6 +7,8 @@ import ( ) func (ms *MarketData) GetFuturePrice(symbol string) (float64, float64, int64, bool) { + ms.mu.RLock() + defer ms.mu.RUnlock() p, ok := ms.futureMarkPrice[symbol] if !ok { return 0, 0, 0, false @@ -23,6 +25,8 @@ func (ms *MarketData) StartFutureWsMarkPrice() error { } func (ms *MarketData) futureWsMarkPriceHandler(event futures.WsAllMarkPriceEvent) { + ms.mu.Lock() + defer ms.mu.Unlock() for _, priceEvent := range event { price, err := strconv.ParseFloat(priceEvent.MarkPrice, 64) if err != nil { @@ -46,5 +50,15 @@ func (ms *MarketData) futureWsErrHandler(err error) { } func (ms *MarketData) GetAllFundRate() (map[string]float64, map[string]int64) { - return ms.futureFundingRate, ms.futureNextFundingTime + ms.mu.RLock() + defer ms.mu.RUnlock() + rates := make(map[string]float64, len(ms.futureFundingRate)) + for k, v := range ms.futureFundingRate { + rates[k] = v + } + times := make(map[string]int64, len(ms.futureNextFundingTime)) + for k, v := range ms.futureNextFundingTime { + times[k] = v + } + return rates, times } diff --git a/internal/data/market/main.go b/internal/data/market/main.go index 19bec1a..5e1e9c3 100644 --- a/internal/data/market/main.go +++ b/internal/data/market/main.go @@ -1,13 +1,24 @@ package market -import "github.com/rs/zerolog/log" +import ( + "sync" + "time" + + "github.com/rs/zerolog/log" +) type MarketData struct { + mu sync.RWMutex futureMarkPrice map[string]float64 futureFundingRate map[string]float64 futureNextFundingTime map[string]int64 spotPrice map[string]float64 + + // Alpha token cache + alphaTokens map[string]AlphaTokenInfo + alphaCacheMutex sync.RWMutex + lastAlphaCacheUpdate time.Time } func NewMarketData() *MarketData { @@ -17,9 +28,14 @@ func NewMarketData() *MarketData { futureFundingRate: make(map[string]float64), futureNextFundingTime: make(map[string]int64), - spotPrice: make(map[string]float64), + spotPrice: make(map[string]float64), + alphaTokens: make(map[string]AlphaTokenInfo), } _ = ms.StartFutureWsMarkPrice() _ = ms.StartSpotWsMarkPrice() + + // Initialize Alpha token cache + go ms.refreshAlphaTokenCache() + return ms } diff --git a/internal/data/market/spot_price.go b/internal/data/market/spot_price.go index cf51795..a56a7db 100644 --- a/internal/data/market/spot_price.go +++ b/internal/data/market/spot_price.go @@ -1,14 +1,35 @@ package market import ( + "strconv" + "github.com/adshao/go-binance/v2" "github.com/rs/zerolog/log" - "strconv" ) func (ms *MarketData) GetSpotPrice(symbol string) (float64, bool) { + ms.mu.RLock() p, ok := ms.spotPrice[symbol] - return p, ok + ms.mu.RUnlock() + if ok { + return p, true + } + + // If not found, check if it's an Alpha token and ensure cache is loaded + ms.ensureAlphaCacheLoaded() + if ms.IsAlphaToken(symbol) { + if alphaToken, exists := ms.GetAlphaToken(symbol); exists { + price := alphaToken.GetPrice() + if price > 0 { + ms.mu.Lock() + ms.spotPrice[symbol] = price + ms.mu.Unlock() + return price, true + } + } + } + + return 0, false } func (ms *MarketData) StartSpotWsMarkPrice() error { @@ -21,6 +42,8 @@ func (ms *MarketData) StartSpotWsMarkPrice() error { } func (ms *MarketData) spotWsAllMarketsStatHandler(event binance.WsAllMarketsStatEvent) { + ms.mu.Lock() + defer ms.mu.Unlock() for _, priceEvent := range event { price, err := strconv.ParseFloat(priceEvent.LastPrice, 64) if err != nil { diff --git a/internal/helper/binancex/symbol.go b/internal/helper/binancex/symbol.go index 62d5f0b..b4a45cc 100644 --- a/internal/helper/binancex/symbol.go +++ b/internal/helper/binancex/symbol.go @@ -1,10 +1,11 @@ package binancex import ( + "strings" + "me.thuanle/bbot/internal/configs/binance" "me.thuanle/bbot/internal/data" "me.thuanle/bbot/internal/utils/stringx" - "strings" ) func Symbol2Token(sym string) string { @@ -74,7 +75,14 @@ func Token2Symbols(token string) []string { } func IsToken(s string) bool { - return len(Token2Symbols(s)) > 0 + // First check regular symbols + if len(Token2Symbols(s)) > 0 { + return true + } + + // Then check Alpha tokens + s = strings.ToUpper(s) + return data.Market.IsAlphaToken(s) } // Future2SpotSymbol convert future symbol to spot symbol diff --git a/internal/services/tele/commands/token.go b/internal/services/tele/commands/token.go index daaef91..bfced49 100644 --- a/internal/services/tele/commands/token.go +++ b/internal/services/tele/commands/token.go @@ -1,6 +1,8 @@ package commands import ( + "strings" + "golang.org/x/text/language" "golang.org/x/text/message" "gopkg.in/telebot.v3" @@ -9,7 +11,6 @@ import ( "me.thuanle/bbot/internal/helper/binancex" "me.thuanle/bbot/internal/services/tele/chat" "me.thuanle/bbot/internal/services/tele/view" - "strings" ) var lastEthPrice float64 @@ -27,6 +28,22 @@ func showStickerMode(context telebot.Context, token string) { } func OnTokenInfoByToken(context telebot.Context, token string) error { + token = strings.ToUpper(token) + + // Check if it's an Alpha token first + if data.Market.IsAlphaToken(token) { + showStickerMode(context, token) + + if alphaToken, exists := data.Market.GetAlphaToken(token); exists { + sp := alphaToken.GetPrice() + // For Alpha tokens, we don't have future price, funding rate, etc. + // Use spot price for both spot and future in the display + _ = chat.ReplyMessage(context, view.RenderOnPriceMessage(token, sp, sp, 0, 0, 0)) + } + return nil + } + + // Regular token handling symbols := binancex.Token2Symbols(token) if len(symbols) == 0 { return nil @@ -36,7 +53,7 @@ func OnTokenInfoByToken(context telebot.Context, token string) error { fp, fundRate, fundTime, ok := data.Market.GetFuturePrice(symbols[0]) marginRates := data.Market.GetMarginInterestRates() - tokenInterestRate := marginRates[strings.ToUpper(token)] + tokenInterestRate := marginRates[token] if !ok { return nil }