diff --git a/.gitignore b/.gitignore index 4257029..aafdc08 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ cmd/*/* !*/cmd/*/*.go .idea .env.example -.env \ No newline at end of file +.env +.DS_Store \ No newline at end of file diff --git a/v2/cmd/migrations/00010_add_token_table.up.sql b/v2/cmd/migrations/00010_add_token_table.up.sql new file mode 100644 index 0000000..82437e3 --- /dev/null +++ b/v2/cmd/migrations/00010_add_token_table.up.sql @@ -0,0 +1,141 @@ +CREATE TABLE IF NOT EXISTS token ( + address TEXT NOT NULL, + chain_id BIGINT NOT NULL, + symbol TEXT NOT NULL, + decimals BIGINT NOT NULL, + PRIMARY KEY(address, chain_id) +); + +CREATE TABLE IF NOT EXISTS maker_name ( + address TEXT NOT NULL, + tag TEXT NOT NULL, + PRIMARY KEY(address) +); + +INSERT INTO maker_name (address, tag) VALUES +('0xfbeedcfe378866dab6abbafd8b2986f5c1768737', 'Symbolic Capital Partners'), +('0x0da9d9ecea7235c999764e34f08499ca424c0177', 'Symbolic Capital Partners'), +('0xfa103c21ea2df71dfb92b0652f8b1d795e51cdef', 'Symbolic Capital Partners'), +('0x5050e08626c499411b5d0e0b5af0e83d3fd82edf', 'Symbolic Capital Partners'), +('0xa69babef1ca67a37ffaf7a485dfff3382056e78c', 'Symbolic Capital Partners: MEV Bot'), +('0xa57bd00134b2850b2a1c55860c9e9ea100fdd6cf', 'Symbolic Capital Partners: MEV Bot'), +('0x18b19edb120300ea1b05964c785692bca89a4bfc', 'ClipperExecutor'), +('0x807cf9a772d5a3f9cefbc1192e939d62f0d9bd38', 'Kyber PMM'), +('0x77d5f03822f94b39ad6117f6a46761ec5879031b', 'Rizzolver (Wintermute)'), +('0xbcc66fc7402daa98f5764057f95ac66b9391cd6b', 'Symbolic Capital Partners: MEV Bot'), +('0xff8ba4d1fc3762f6154cc942ccf30049a2a0cec6', 'Tokka Labs (Altonomy): MM'), +('0xbb289bc97591f70d8216462df40ed713011b968a', 'Caladan (Alphalab Capital): MM'), +('0x2c6bea966e83dff8619e54fd819da727ed5102e1', 'Trusted Volumes'), +('0xb6613cc55866e282638006455390207c1d485be9', 'DEX Taker'), +('0x28a55c4b4f9615fde3cdaddf6cc01fcf2e38a6b0', 'DEX Taker'), +('0x028615cfb66217db887b7d8b406df2f436cf85d1', 'Martian88 on OpenSea'), +('0xbad9ada0e9633ed27fa183dbdeceef76712a6ee1', 'Onebit'), +('0xde574553f9feecc969ef2ef1f42bc9e27f8171a2', 'OpenSea User'), +('0x4306d7a79265d2cb85db0c5a55ea5f4f6f73c4b1', 'kiv1n on OpenSea'), +('0x9e64508d3986be7c95796482d4890c9f1a93c27d', 'OpenSea User'), +('0xb291b62e7168b6227ab2cb30e683aff9db9076a8', 'xtokenpay.eth'), +('0x51c72848c68a965f66fa7a88855f9f7784502a7f', 'Wintermute: MM'), +('0x0000006daea1723962647b7e189d311d757fb793', 'Wintermute: MM'), +('0x5d47603902a8cadbf3337abbc6df8f67a251d821', 'Wintermute: MM'), +('0x0000f079e68bbcc79ab9600ace786b0a4db1c83c', 'Wintermute: MM'), +('0xdbf5e9c5206d0db70a90108bf936da60221dc080', 'Wintermute: MM'), +('0x225a38bc71102999dd13478bfabd7c4d53f2dc17', 'Wintermute: MM'), +('0x442a03356d0203756e10c8838d9bcf571863a9a1', 'Hashflow: Trusted Volumes MM'), +('0x4a32007040567190bed4fa6ff3f5a03c0d47c9d7', 'harrigan.eth'), +('0x47661305e8f4ac76ecaf47f7657c6efc5833f9de', 'bananaskin.eth'), +('0x5922ec9825ca375d5e36b3d600afb76bb85dfa58', 'OpenSea User'), +('0x56178a0d5f301baf6cf3e1cd53d9863437345bf9', 'Symbolic Capital Partners: MM'), +('0xad8852ad213b3a24d8e69b22c7425f19259db65e', 'Top_Domains on OpenSea'), +('0xbd852bbd167df0c4575fa8ba46ec33c7431958de', 'OpenSea User'), +('0x6f1cdbbb4d53d226cf4b917bf768b94acbab6168', 'Mev Bot'), +('0x24b9d98fabf4da1f69ee10775f240ae3da6856fd', 'Hashflow: Kyber PMM'), +('0x9d32ccb03147e88f0d84dcd846e5bceadfa9bdd0', 'tithuhai.eth'), +('0xbf19cbf0256f19f39a016a86ff3551ecc6f2aafe', 'Blockin'), +('0x85c7a66b783c22df77cc8a25ac149d7d49ea5850', 'stal1n.eth'), +('0x61a4aa7ee787e19f1a3a82c1021d5b88311dcb9d', 'n0xsfera.eth'), +('0x7bef7fff4b2dcfc39ed2ab73a9c4a8c5346b5282', '@takachan114: ishihama.eth'), +('0x0eaa8e74a4ab0db986bacd48d92787919588ea1c', 'milaenss.eth'), +('0x9d0068a025c956e2e961a07d047c058d37bdcc57', '我玩一号位我想打点钱.eth'), +('0x4ab44ba5ba6c8f1b382c03752eb310afd9b0e101', 'Hashflow: Tokka Labs'), +('0xb4fb31e7b1471a8e52dd1e962a281a732ead59c1', '@chud_eth: chud.eth'), +('0x2807c205bedc4361d913eabd02d30f4a0d64e559', '@JohnDoe57306855: olexandr.eth'), +('0xa766eddd4a4d43eba40ea1ea4cbdb9835077df44', 'KaoloSafe on OpenSea'), +('0xd2aeb505b4867efc0afa8f49021fefb7ef9c9fdd', 'BobLed on OpenSea'), +('0x1160fb1b4c936170b5f2163e03c263b5b235cbc0', 'vineetpant.eth'), +('0x0558c59cd74d1cbf12e9337e33732a83040592a7', 'alexholden.eth'), +('0x3b7c36246579d480240a61ae220b596dcbadd6bb', 'OpenSea User'), +('0x4f2aec2bec8127298c17bc006284eea27d83e87c', 'Sduff on OpenSea'), +('0xb024ec90398fd2c1e2b77ef25d7d55bee9b60176', 'fshem.eth'), +('0xfa1a94ae136880002ff4b4665ab4e7498973332f', 'DEX Taker'), +('0x812297e64a928ebe4025d3a590ecce6756ccde7d', 'OpenSea User'), +('0x0f263ec66cbb3848d301fa38279c852b9a7aa2b1', 'OpenSea User'), +('0x713dc4df480235dbe2fb766e7120cbd4041dcb58', 'Hashflow: Xyzeth'), +('0x4dbb951c9c137adf96e948beecc63892cb62aa87', 'OpenSea User'), +('0x6b12867744200e52a7b50318180872b3c81b5635', 'OpenSea User'), +('0x7e3de19f01cca63953355126ca3b2071968b8d59', 'Odos?: MEV Bot'), +('0x5ea12341d073ec5a1226b85f0478413a19081535', '@cryptopunk3129?: punksvault.eth'), +('0xb3475eff5add9099b62295951be9c062cb7d04df', 'OpenSea User'), +('0x031903307c517c11b71f8313d19afde0a4f41cb5', 'Hashflow: Caladan'), +('0x29f3f0ac208e3c5cacdf3d5dcbd3bc0466a08e81', 'MarketMakerProxy'), +('0x47c3eb3dcdd524c2508da649d08f94571dcd398a', 'OpenSea User'), +('0x44bdb19db1cd29d546597af7dc0549e7f6f9e480', 'konpyl on OpenSea'), +('0xeca97595b3f6feb638c6b3444d408b40615e8c86', 'itspossiblen on OpenSea'), +('0x80820f9cd3687ea10a0f14c875b2b7edfdc18ce5', 'FastFooDik on OpenSea'), +('0x721379c5d0a6400d64d84b7a168befd60f41a037', 'CryptoPyxies on OpenSea'), +('0x120ebfe81c44003cd5351731709e30f9681daf74', 'rnotoh.eth'), +('0x099be958c17c40491d6d7f08a79466b67677ec6c', 'OpenSea User'), +('0x1bb168cafc4422cf67445d1eab6879a06fb1aff7', '@iamhumidity : OpenSea User'), +('0x2e4239e4464ca2f7980b48ceb42868247bb3a23c', 'OpenSea User'), +('0x802973801c49113f006702391d615acdd3dce768', 'compte2.eth'), +('0x235a2ac113014f9dcb8aba6577f20290832ddefd', 'OpenSea User'), +('0x05b6ef41449dfdebe54c81fa272eaf8532c903f1', 'ape-capital.eth'), +('0x9b1565c4c55c6c740e1d5682bb6c2ace9aca18ed', '8aa715 on OpenSea'), +('0xfa040a07183a456e9c2210287330cba168e123c8', 'OpenSea User'), +('0xc1cf36c0932719a36521803666205d8dddd7142b', 'OpenSea User'), +('0xe6b01245b503fd8e3353e1a2996d539b3ae13f30', 'DigitalRenaissance on OpenSea'), +('0x52173a13b25eddbd9599c233f67f68e49a782f18', 'olekasus.eth'), +('0x51f5a4ae44e90c1212c29927216296f29151f4d8', 'p00rfag.eth'), +('0x9b0de76cb0a21cad9fd37675731f65773508af9e', 'cudam321.eth'), +('0x219eb6c4b8be17e47f5a555e377077470d485f81', 'fanyana.eth'), +('0xd5b696bcd66047f577699b67090d75749beb75d4', 'biglit.eth'), +('0xa12c8eaced4109a836011d7e88a4d658e03a20b9', 'a314whatever.eth'), +('0x68bf97559ed899ab425ac0acc925c3042de75616', 'OpenSea User'), +('0x49e96e255ba418d08e66c35b588e2f2f3766e1d0', 'OpenSea User'), +('0x854f1269b659a727a2268ab86ff77cfb30bfb358', 'invictus369.eth'), +('0x9591e8a4ab3811e48e9f852b5f1daca351785361', '醉后不知天在水满船清梦压星河.eth'), +('0x161619839ebbada5d3c279e1a79c50f7fc4274d6', '@threef0921 : threef.eth'), +('0x10b7dea91297773efb697664f47c1b1932908888', 'linkzk.eth'), +('0x3e6eec7992011297e8e05a2a0b58046f209b08ec', 'moonlitmelody.eth'), +('0xc8c39af7983bf329086de522229a7be5fc4e41cc', 'Hashflow: Wintermute'), +('0xb46166ed8efef7fa57b77219397607403abf5253', 'overtone on OpenSea'), +('0x6c3f1e47f2326ea3fc49b88922192f9348983eda', 'OpenSea User'), +('0xfd7b79f8ee41fe94d31ffaeaa5e7f37d13bf7fa5', 'ibrahim.umbra.eth'), +('0x47396f3bc72e4daa8f3ea44dad448177cf5b0a3b', 'Miller30 on OpenSea'), +('0x05303c2d6b8214a81da1585a0e29099a170e4b5a', 'MR_1_BIT on OpenSea'), +('0x100ed6fa16809285390167d9b0e03d56ea6c617a', 'CamelotV3ViewHelper'), +('0x3dc5fcb0ad5835c6059112e51a75b57dba668eb8', 'Mantle : Deployer'), +('0x9184f79be89b8d86a960d03ec2ae40292dbcfa46', 'NFTinitcom_0000 on OpenSea'), +('0x64fd9fdd70a4a958dacb642e6c706bd7226b5d95', 'OpenSea User'), +('0x722e895a12d2a11be99ed69dbc1fedbb9f3cd8fe', 'rationalman.eth'), +('0xa447b855719c3114545b10b29aabcc4955f99d67', '@dorkonforty'), +('0xb85971b04fe023e961eab91d3c60c7eb4ab66a31', 'OpenSea User'), +('0xb02f39e382c90160eb816de5e0e428ac771d77b5', 'Trusted Volumes'), +('0x3d03dc9b0a596bfa5e5c278c0886934b6416afaa', 'High Balance'), +('0x2eaff50012018d78d61d4f0ba8eb153adaef7238', 'Heavy DEX Trader'), +('0x5a4853688e71e437e17be0f8ca41277013a8fdb7', 'High Balance'), +('0xfeeda5a996a6e603b64d113460723a3443960169', 'High Balance'), +('0x79c34141163fd4fb55fd6f8e0c97108772e979f9', 'High Balance'), +('0xce5be4b74b6217d14606c744ec8024562a56e406', 'High Balance'), +('0xfcb51642a2a33eafefd79c236480e295ccbd4a44', 'UniX Solver'), +('0x0b6f221c21e92ee2e20c3fe1b4890e9f71ece05a', 'High Gas Consumer'), +('0xb62a80b0a96a3df1fbc13d5e2a0bd967c997eead', 'Heavy DEX Trader'), +('0x79e40ab4bac23e2910c03e2fc24819fe498a9491', 'Medium DEX Trader'), +('0x070f13a1b29f875fc0b8e504c4318d2a271d124e', 'DEX Trader'), +('0xbfa899c1ad97229d9c604e9ea927c7acb988c05c', 'Hashflow: Wintermute'), +('0x111bb8c3542f2b92fb41b8d913c01d3788431111', 'Xyzeth'), +('0xd198fbe60c49d4789525fc54567447518c7d2a11', 'ETH FlowTraders MM'), +('0x7913005b548839da13765020ddb9bf74a151b327', 'ETH FlowTraders MM'), +('0x32801ab1957aaad1c65289b51603373802b4e8bb', 'ETH FlowTraders MM'), +('0xd85351181b3f264ee0fdfa94518464d7c3defada', 'James Fickel'), +('0x049a6f30a91cdad18473739a69f957175acb83d3', 'Trusted Volumes'), +('0x67336cec42645f55059eff241cb02ea5cc52ff86', 'MX Trading'); \ No newline at end of file diff --git a/v2/cmd/price_filler/main.go b/v2/cmd/price_filler/main.go index df1e232..e1f41f5 100644 --- a/v2/cmd/price_filler/main.go +++ b/v2/cmd/price_filler/main.go @@ -3,11 +3,13 @@ package main import ( "fmt" "log" + "net/http" "os" - "github.com/KyberNetwork/go-binance/v2" libapp "github.com/KyberNetwork/tradelogs/v2/pkg/app" - "github.com/KyberNetwork/tradelogs/v2/pkg/price_filler" + "github.com/KyberNetwork/tradelogs/v2/pkg/mtm" + pricefiller "github.com/KyberNetwork/tradelogs/v2/pkg/price_filler" + dashboardStorage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/dashboard" bebopStorage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/bebop" hashflowv3Storage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/hashflow_v3" kyberswapStorage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/kyberswap" @@ -25,6 +27,8 @@ import ( "go.uber.org/zap" ) +// This week I will deploy the new price filler of tradelog v2, which calls my new mark to market. +// After deploying, I will have data to continue creating competition dashboard. func main() { app := libapp.NewApp() app.Name = "trade logs crawler service" @@ -68,9 +72,10 @@ func run(c *cli.Context) error { zxrfqv3Storage.New(l, db), pancakeswapStorage.New(l, db), } - - binanceClient := binance.NewClient(c.String(libapp.BinanceAPIKeyFlag.Name), c.String(libapp.BinanceSecretKeyFlag.Name)) - priceFiller, err := pricefiller.NewPriceFiller(l, binanceClient, s) + httpClient := &http.Client{} + mtmClient := mtm.NewMtmClient(c.String(libapp.MarkToMarketURLFlag.Name), httpClient) + dashboardStorage := dashboardStorage.New(l, db) + priceFiller, err := pricefiller.NewPriceFiller(l, s, mtmClient, dashboardStorage) if err != nil { l.Errorw("Error while init price filler") return err diff --git a/v2/cmd/tradelogs/main.go b/v2/cmd/tradelogs/main.go index efa3291..395d284 100644 --- a/v2/cmd/tradelogs/main.go +++ b/v2/cmd/tradelogs/main.go @@ -7,6 +7,7 @@ import ( "github.com/KyberNetwork/tradelogs/v2/internal/server" libapp "github.com/KyberNetwork/tradelogs/v2/pkg/app" + dashboardStorage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/dashboard" bebopStorage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/bebop" hashflowv3Storage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/hashflow_v3" kyberswapStorage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/kyberswap" @@ -67,8 +68,8 @@ func run(c *cli.Context) error { zxrfqv3Storage.New(l, db), pancakeswapStorage.New(l, db), } - - s := server.NewTradeLogs(l, storage, c.String(libapp.HTTPTradeLogsServerFlag.Name)) + dashStorage := dashboardStorage.New(l, db) + s := server.NewTradeLogs(l, storage, dashStorage, c.String(libapp.HTTPTradeLogsServerFlag.Name)) return s.Run() } diff --git a/v2/internal/server/tradelogs.go b/v2/internal/server/tradelogs.go index 1ee936b..662d00b 100644 --- a/v2/internal/server/tradelogs.go +++ b/v2/internal/server/tradelogs.go @@ -5,6 +5,8 @@ import ( "net/http" "time" + dashboardStorage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/dashboard" + dashboardTypes "github.com/KyberNetwork/tradelogs/v2/pkg/storage/dashboard/types" storageTypes "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/types" "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" @@ -16,21 +18,28 @@ var ( ) type TradeLogs struct { - r *gin.Engine - bindAddr string - l *zap.SugaredLogger - storage []storageTypes.Storage + r *gin.Engine + bindAddr string + l *zap.SugaredLogger + storage []storageTypes.Storage + dashStorage *dashboardStorage.Storage } -func NewTradeLogs(l *zap.SugaredLogger, s []storageTypes.Storage, bindAddr string) *TradeLogs { +func NewTradeLogs( + l *zap.SugaredLogger, + s []storageTypes.Storage, + dashStorage *dashboardStorage.Storage, + bindAddr string, +) *TradeLogs { engine := gin.New() engine.Use(gin.Recovery()) server := &TradeLogs{ - r: engine, - bindAddr: bindAddr, - l: l, - storage: s, + r: engine, + bindAddr: bindAddr, + l: l, + storage: s, + dashStorage: dashStorage, } gin.SetMode(gin.ReleaseMode) @@ -51,6 +60,9 @@ func (s *TradeLogs) Run() error { func (s *TradeLogs) register() { pprof.Register(s.r, "/debug") s.r.GET("/tradelogs", s.getTradeLogs) + s.r.GET("/tokens", s.getTokens) + s.r.GET("/makers", s.getMakerName) + s.r.POST("/add_makers", s.addMakerName) } func (s *TradeLogs) getTradeLogs(c *gin.Context) { @@ -91,3 +103,52 @@ func (s *TradeLogs) getTradeLogs(c *gin.Context) { "data": data, }) } + +func (s *TradeLogs) getTokens(c *gin.Context) { + var queries dashboardTypes.TokenQuery + if err := c.ShouldBind(&queries); err != nil { + responseErr(c, http.StatusBadRequest, err) + return + } + + data, err := s.dashStorage.GetTokens(queries) + if err != nil { + responseErr(c, http.StatusBadRequest, err) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": data, + }) +} + +func (s *TradeLogs) getMakerName(c *gin.Context) { + data, err := s.dashStorage.GetMakerName() + if err != nil { + responseErr(c, http.StatusBadRequest, err) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": data, + }) +} + +func (s *TradeLogs) addMakerName(c *gin.Context) { + var queries []dashboardTypes.MakerName + + if err := c.ShouldBindJSON(&queries); err != nil { + responseErr(c, http.StatusBadRequest, err) + return + } + + if err := s.dashStorage.InsertMakerName(queries); err != nil { + responseErr(c, http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": queries, + }) +} diff --git a/v2/pkg/app/price_filler.go b/v2/pkg/app/price_filler.go index 9027785..a5caadf 100644 --- a/v2/pkg/app/price_filler.go +++ b/v2/pkg/app/price_filler.go @@ -13,10 +13,15 @@ var BinanceSecretKeyFlag = cli.StringFlag{ Name: "binance-secret-key", EnvVar: "BINANCE_SECRET_KEY", } +var MarkToMarketURLFlag = cli.StringFlag{ + Name: "mark-to-market-url", + EnvVar: "MARK_TO_MARKET_URL", +} func PriceFillerFlags() []cli.Flag { return []cli.Flag{ BinanceAPIKeyFlag, BinanceSecretKeyFlag, + MarkToMarketURLFlag, } } diff --git a/v2/pkg/mtm/client.go b/v2/pkg/mtm/client.go new file mode 100644 index 0000000..d31c64e --- /dev/null +++ b/v2/pkg/mtm/client.go @@ -0,0 +1,99 @@ +package mtm + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "github.com/KyberNetwork/tradinglib/pkg/httpclient" +) + +type MtmClient struct { + baseURL string + httpClient *http.Client +} + +func NewMtmClient(baseURL string, httpClient *http.Client) *MtmClient { + return &MtmClient{ + baseURL: strings.TrimRight(baseURL, "/"), + httpClient: httpClient, + } +} + +type Token struct { + Address string `json:"address"` + ChainId int64 `json:"chain_id"` + Symbol string `json:"symbol"` + Decimals int64 `json:"decimals"` +} +type TokenResponse struct { + Success bool `json:"success"` + Data []Token `json:"data"` +} + +func (m *MtmClient) GetListTokens(ctx context.Context) ([]Token, error) { + const path = "/tokens" + + httpReq, err := httpclient.NewRequestWithContext( + ctx, + http.MethodGet, + m.baseURL, + path, + nil, + nil) + if err != nil { + return nil, fmt.Errorf("new request error: %w", err) + } + + var resp TokenResponse + if _, err := httpclient.DoHTTPRequest( + m.httpClient, + httpReq, + &resp, + httpclient.WithStatusCode(http.StatusOK)); err != nil { + return nil, fmt.Errorf("do http request error: %w", err) + } + + return resp.Data, nil +} + +type RateV3Response struct { + Success bool `json:"success"` + Data struct { + Price float64 `json:"price"` + TimeUnix int64 `json:"timeUnix"` + } `json:"data"` +} + +func (m *MtmClient) GetHistoricalRate( + ctx context.Context, base, quote string, chainId int64, ts time.Time, +) (float64, error) { + const path = "/v3/historical" + httpReq, err := httpclient.NewRequestWithContext( + ctx, + http.MethodGet, + m.baseURL, + path, + httpclient.NewQuery(). + SetString("base", base). + SetString("quote", quote). + Int64("chain_id", chainId). + Int64("time", ts.Unix()), + nil) + if err != nil { + return 0, fmt.Errorf("new request error: %w", err) + } + + var rate RateV3Response + if _, err := httpclient.DoHTTPRequest( + m.httpClient, + httpReq, + &rate, + httpclient.WithStatusCode(http.StatusOK)); err != nil { + return 0, fmt.Errorf("do http request error: %w", err) + } + + return rate.Data.Price, nil +} diff --git a/v2/pkg/mtm/client_test.go b/v2/pkg/mtm/client_test.go new file mode 100644 index 0000000..19e1d84 --- /dev/null +++ b/v2/pkg/mtm/client_test.go @@ -0,0 +1,25 @@ +package mtm + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/test-go/testify/require" +) + +func TestNewMtmClient(t *testing.T) { + // need mtm url + t.Skip() + MTM_URL := "" + httpClient := &http.Client{} + client := NewMtmClient(MTM_URL, httpClient) + rate, err := client.GetHistoricalRate(context.Background(), "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "0xdac17f958d2ee523a2206206994597c13d831ec7", 1, time.UnixMilli(1732870268000)) + require.NoError(t, err) + fmt.Println("historical rate", rate) + + _, err = client.GetListTokens(context.Background()) + require.NoError(t, err) +} diff --git a/v2/pkg/price_filler/ks_client.go b/v2/pkg/price_filler/ks_client.go index 38dba3c..813a8a6 100644 --- a/v2/pkg/price_filler/ks_client.go +++ b/v2/pkg/price_filler/ks_client.go @@ -85,13 +85,15 @@ type TokenCatalogResp struct { } type TokenCatalog struct { - Decimals int64 `json:"decimals"` + Decimals int64 `json:"decimals"` + Symbol string `json:"symbol"` + ChainId int16 `json:"chainId"` } func (c *KsClient) GetTokenCatalog(address string) (TokenCatalogResp, error) { var resp TokenCatalogResp err := c.DoRequest(context.Background(), http.MethodGet, - fmt.Sprintf("%s/tokens?chainIds=%d&query=%s", c.baseURL, NetworkETHChanID, address), + fmt.Sprintf("%s/tokens?chainIds=%d&query=%s", c.baseURL, NetworkETHChainID, address), nil, &resp) if err != nil { return TokenCatalogResp{}, err @@ -113,12 +115,19 @@ type ImportTokenParam struct { Tokens []ImportedToken `json:"tokens"` } +type TokenCatalogImportResp struct { + Decimals int64 `json:"decimals"` + ChainId string `json:"chainId"` + Address string `json:"address"` + Symbol string `json:"symbol"` +} + type ImportTokenResp struct { Code int64 `json:"code"` Message string `json:"message"` Data struct { Tokens []struct { - Data TokenCatalog `json:"data"` + Data TokenCatalogImportResp `json:"data"` } `json:"tokens"` } `json:"data"` } diff --git a/v2/pkg/price_filler/price_filler.go b/v2/pkg/price_filler/price_filler.go index e406975..1eeca00 100644 --- a/v2/pkg/price_filler/price_filler.go +++ b/v2/pkg/price_filler/price_filler.go @@ -3,20 +3,21 @@ package pricefiller import ( "context" "errors" - "github.com/KyberNetwork/tradelogs/v2/pkg/constant" - "strconv" "strings" "sync" "time" - "github.com/KyberNetwork/go-binance/v2" + "github.com/KyberNetwork/tradelogs/v2/pkg/constant" + "github.com/KyberNetwork/tradelogs/v2/pkg/mtm" + dashboardStorage "github.com/KyberNetwork/tradelogs/v2/pkg/storage/dashboard" + dashboardTypes "github.com/KyberNetwork/tradelogs/v2/pkg/storage/dashboard/types" storageTypes "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/types" "go.uber.org/zap" ) const ( - NetworkETHChanID = 1 - NetworkETHChanIDString = "1" + NetworkETHChainID = 1 + NetworkETHChainIDString = "1" NetworkETH = "ETH" updateAllCoinInfoInterval = 12 * time.Hour backfillTradeLogsPriceInterval = 10 * time.Minute @@ -24,56 +25,57 @@ const ( addressETH1 = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" addressETH2 = "0x0000000000000000000000000000000000000000" coinUSDT = "USDT" + USDTAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7" invalidSymbolErrString = " code=-1121, msg=Invalid symbol." ) var ( ErrNoPrice = errors.New("no price from binance") ErrWeirdTokenCatalogResp = errors.New("weird token catalog response") - - mappedMultiplier = map[string]float64{ - "1MBABYDOGE": 1e-6, - "1000SATS": 1e-3, - } ) type CoinInfo struct { Coin string - Network string + ChainId int64 ContractAddress string Decimals int64 } type PriceFiller struct { - l *zap.SugaredLogger - s []storageTypes.Storage - mu sync.Mutex - ksClient *KsClient - binanceClient *binance.Client - mappedCoinInfo map[string]CoinInfo // address - coinInfo + l *zap.SugaredLogger + s []storageTypes.Storage + mu sync.Mutex + ksClient *KsClient + mappedCoinInfo map[string]CoinInfo // address - coinInfo + mtmClient *mtm.MtmClient + dashboardStorage *dashboardStorage.Storage } -func NewPriceFiller(l *zap.SugaredLogger, binanceClient *binance.Client, - s []storageTypes.Storage) (*PriceFiller, error) { +func NewPriceFiller(l *zap.SugaredLogger, + s []storageTypes.Storage, + mtmClient *mtm.MtmClient, + dashboardStorage *dashboardStorage.Storage, +) (*PriceFiller, error) { p := &PriceFiller{ - l: l, - s: s, - ksClient: NewKsClient(), - binanceClient: binanceClient, + l: l, + s: s, + ksClient: NewKsClient(), mappedCoinInfo: map[string]CoinInfo{ addressETH1: { Coin: "ETH", - Network: NetworkETH, + ChainId: NetworkETHChainID, ContractAddress: addressETH1, Decimals: 18, }, addressETH2: { Coin: "ETH", - Network: NetworkETH, + ChainId: NetworkETHChainID, ContractAddress: addressETH2, Decimals: 18, }, }, + mtmClient: mtmClient, + dashboardStorage: dashboardStorage, } if err := p.updateAllCoinInfo(); err != nil { @@ -88,32 +90,21 @@ func (p *PriceFiller) Run() { } func (p *PriceFiller) getPrice(token string, timestamp int64) (float64, error) { - candles, err := p.binanceClient.NewKlinesService().Symbol(withAlias(token) + "USDT"). - Interval("1s").StartTime(timestamp).EndTime(timestamp).Do(context.Background()) - if err != nil { - return 0, err - } - if len(candles) == 0 { - return 0, ErrNoPrice - } - low, err := strconv.ParseFloat(candles[0].Low, 64) - if err != nil { - return 0, err - } - high, err := strconv.ParseFloat(candles[0].High, 64) + price, err := p.mtmClient.GetHistoricalRate( + context.Background(), + token, + USDTAddress, + NetworkETHChainID, + time.UnixMilli(timestamp), + ) if err != nil { return 0, err } - multiplier := 1.0 - if m, ok := mappedMultiplier[token]; ok { - multiplier = m - } - - return multiplier * (low + high) / 2, nil + return price, nil } func (p *PriceFiller) updateAllCoinInfo() error { - resp, err := p.binanceClient.NewAllCoinService().Do(context.Background()) + tokens, err := p.mtmClient.GetListTokens(context.Background()) if err != nil { p.l.Errorw("Failed to get all coins info", "err", err) return err @@ -121,23 +112,19 @@ func (p *PriceFiller) updateAllCoinInfo() error { p.mu.Lock() defer p.mu.Unlock() - for _, coinInfo := range resp { - for _, network := range coinInfo.NetworkList { - if network.Network == NetworkETH && network.ContractAddress != "" { - address := strings.ToLower(network.ContractAddress) - if _, ok := p.mappedCoinInfo[address]; !ok { - p.mappedCoinInfo[address] = CoinInfo{ - Coin: network.Coin, - Network: network.Network, - ContractAddress: address, - } - } - break - } + for _, info := range tokens { + if info.ChainId != NetworkETHChainID { + continue + } + p.mappedCoinInfo[info.Address] = CoinInfo{ + Coin: info.Symbol, + ChainId: info.ChainId, + ContractAddress: info.Address, + Decimals: info.Decimals, } - } - p.l.Infow("New mapped coin info", "data", p.mappedCoinInfo) + } + p.l.Infow("Successfully map coin info") return nil } @@ -172,6 +159,11 @@ func (p *PriceFiller) runBackFillTradelogPriceRoutine() { p.l.Infow("backfill tradelog price successfully", "exchange", s.Exchange(), "number", len(tradeLogs)) } + if err := p.insertTokens(); err != nil { + p.l.Errorw("Failed to insert tokens", "err", err) + continue + } + } } @@ -254,7 +246,7 @@ func (p *PriceFiller) getPriceAndAmountUsd(address, rawAmt string, at int64) (fl p.mu.Unlock() if ok { if coin.Decimals == 0 { - d, err := p.getDecimals(address) + decimals, symbol, err := p.getDecimalsAndSymbol(address) if err != nil { if errors.Is(err, ErrWeirdTokenCatalogResp) { return 0, 0, nil @@ -262,7 +254,8 @@ func (p *PriceFiller) getPriceAndAmountUsd(address, rawAmt string, at int64) (fl p.l.Errorw("Failed to getDecimals", "err", err, "address", address) return 0, 0, err } - coin.Decimals = d + coin.Decimals = decimals + coin.Coin = symbol p.mu.Lock() p.mappedCoinInfo[address] = coin p.mu.Unlock() @@ -271,12 +264,10 @@ func (p *PriceFiller) getPriceAndAmountUsd(address, rawAmt string, at int64) (fl if coin.Coin == coinUSDT { return 1, calculateAmountUsd(rawAmt, coin.Decimals, 1), nil } - price, err := p.getPrice(coin.Coin, at) + price, err := p.getPrice(address, at) if err != nil { - if !errors.Is(err, ErrNoPrice) { - p.l.Errorw("Failed to getPrice", "err", err, "coin", coin.Coin, "at", at) - return 0, 0, err - } + p.l.Errorw("Failed to getPrice", "err", err, "coin", coin.Coin, "at", at) + return 0, 0, err } return price, calculateAmountUsd(rawAmt, coin.Decimals, price), nil @@ -301,31 +292,48 @@ func (p *PriceFiller) FullFillTradeLogs(tradeLogs []storageTypes.TradeLog) { } } -func (p *PriceFiller) getDecimals(address string) (int64, error) { +func (p *PriceFiller) getDecimalsAndSymbol(address string) (int64, string, error) { resp, err := p.ksClient.GetTokenCatalog(address) if err != nil { p.l.Errorw("Failed to GetTokenCatalog", "err", err) - return 0, err + return 0, "", err } - if len(resp.Data.Tokens) == 1 { - return resp.Data.Tokens[0].Decimals, nil + return resp.Data.Tokens[0].Decimals, resp.Data.Tokens[0].Symbol, nil } if len(resp.Data.Tokens) > 1 { p.l.Warnw("Weird token catalog response", "resp", resp) - return 0, ErrWeirdTokenCatalogResp + return 0, "", ErrWeirdTokenCatalogResp } // try to import token if token is not found. - newResp, err := p.ksClient.ImportToken(NetworkETHChanIDString, address) + newResp, err := p.ksClient.ImportToken(NetworkETHChainIDString, address) if err != nil { p.l.Errorw("Failed to ImportToken", "err", err) - return 0, err + return 0, "", err } if len(newResp.Data.Tokens) == 1 { - return newResp.Data.Tokens[0].Data.Decimals, nil + return newResp.Data.Tokens[0].Data.Decimals, newResp.Data.Tokens[0].Data.Symbol, nil } p.l.Warnw("Weird ImportToken response", "resp", newResp) - return 0, ErrWeirdTokenCatalogResp + return 0, "", ErrWeirdTokenCatalogResp + +} + +func (p *PriceFiller) insertTokens() error { + var tokenList []dashboardTypes.Token + for address, token := range p.mappedCoinInfo { + tokenList = append(tokenList, dashboardTypes.Token{ + Address: address, + Symbol: token.Coin, + Decimals: token.Decimals, + ChainId: NetworkETHChainID, + }) + } + err := p.dashboardStorage.InsertTokens(tokenList) + if err != nil { + return err + } + return nil } diff --git a/v2/pkg/price_filler/price_filler_test.go b/v2/pkg/price_filler/price_filler_test.go index cc862fd..ed8c094 100644 --- a/v2/pkg/price_filler/price_filler_test.go +++ b/v2/pkg/price_filler/price_filler_test.go @@ -2,9 +2,10 @@ package pricefiller import ( "encoding/json" + "net/http" "testing" - "github.com/KyberNetwork/go-binance/v2" + "github.com/KyberNetwork/tradelogs/v2/pkg/mtm" "github.com/KyberNetwork/tradelogs/v2/pkg/storage/tradelogs/types" "github.com/test-go/testify/require" "go.uber.org/zap" @@ -12,12 +13,11 @@ import ( // go test -v -timeout 30s -run ^TestFillPrice$ github.com/KyberNetwork/tradelogs/pkg/pricefiller func TestFillPrice(t *testing.T) { - t.Skip("Need to add Binance credentials") - bClient := binance.NewClient("", "") - filler, err := NewPriceFiller(zap.S(), bClient, nil) - if err != nil { - require.NoError(t, err) - } + t.Skip("Need to add mtm url") + httpClient := &http.Client{} + mtmClient := mtm.NewMtmClient("", httpClient) + filler, err := NewPriceFiller(zap.S(), nil, mtmClient, nil) + require.NoError(t, err) tradeLogs := []types.TradeLog{ { @@ -67,12 +67,11 @@ func TestFillPrice(t *testing.T) { } func TestFillBebopPrice(t *testing.T) { - //t.Skip("Need to add Binance credentials") - bClient := binance.NewClient("", "") - filler, err := NewPriceFiller(zap.S(), bClient, nil) - if err != nil { - require.NoError(t, err) - } + t.Skip("Need to add mtm url") + httpClient := &http.Client{} + mtmClient := mtm.NewMtmClient("", httpClient) + filler, err := NewPriceFiller(zap.S(), nil, mtmClient, nil) + require.NoError(t, err) rawTradelogs := []byte(`[{"exchange":"","order_hash":"52344126666663492359326433927181107201","maker":"0x51c72848c68a965f66fa7a88855f9f7784502a7f","taker":"0x589c86cc1f6043f99222843d397ea4e770841cae","maker_token":"0xba100000625a3754423978a60c9317c58a424e3d","taker_token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","maker_token_amount":"418270652861628178","taker_token_amount":"3500000000000000","contract_address":"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F","block_number":20153388,"tx_hash":"0xff46ac555ec7da7aa484864dc0df90217b7f46dfa51627c20ef6e25451c64b15","log_index":133,"timestamp":1730258461000,"event_hash":"0xadd7095becdaa725f0f33243630938c861b0bba83dfd217d4055701aa768ec2e","expiration_date":1719133686,"maker_token_price":0,"taker_token_price":0,"maker_usd_amount":0,"taker_usd_amount":0,"state":""},{"exchange":"","order_hash":"52344126666663492359326433927181107201","maker":"0x51c72848c68a965f66fa7a88855f9f7784502a7f","taker":"0x589c86cc1f6043f99222843d397ea4e770841cae","maker_token":"0x6b175474e89094c44da98b954eedeac495271d0f","taker_token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","maker_token_amount":"1320463992019695220","taker_token_amount":"3500000000000000","contract_address":"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F","block_number":20153388,"tx_hash":"0xff46ac555ec7da7aa484864dc0df90217b7f46dfa51627c20ef6e25451c64b15","log_index":133,"trade_index":1,"timestamp":1730258461000,"event_hash":"0xadd7095becdaa725f0f33243630938c861b0bba83dfd217d4055701aa768ec2e","expiration_date":1719133686,"maker_token_price":0,"taker_token_price":0,"maker_usd_amount":0,"taker_usd_amount":0,"state":""},{"exchange":"","order_hash":"52344126666663492359326433927181107201","maker":"0x51c72848c68a965f66fa7a88855f9f7784502a7f","taker":"0x589c86cc1f6043f99222843d397ea4e770841cae","maker_token":"0xc00e94cb662c3520282e6f5717214004a7f26888","taker_token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","maker_token_amount":"26747477356835299","taker_token_amount":"3500000000000000","contract_address":"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F","block_number":20153388,"tx_hash":"0xff46ac555ec7da7aa484864dc0df90217b7f46dfa51627c20ef6e25451c64b15","log_index":133,"trade_index":2,"timestamp":1730258461000,"event_hash":"0xadd7095becdaa725f0f33243630938c861b0bba83dfd217d4055701aa768ec2e","expiration_date":1719133686,"maker_token_price":0,"taker_token_price":0,"maker_usd_amount":0,"taker_usd_amount":0,"state":""},{"exchange":"","order_hash":"52344126666663492359326433927181107201","maker":"0x51c72848c68a965f66fa7a88855f9f7784502a7f","taker":"0x589c86cc1f6043f99222843d397ea4e770841cae","maker_token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","taker_token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","maker_token_amount":"1320537","taker_token_amount":"3500000000000000","contract_address":"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F","block_number":20153388,"tx_hash":"0xff46ac555ec7da7aa484864dc0df90217b7f46dfa51627c20ef6e25451c64b15","log_index":133,"trade_index":3,"timestamp":1730258461000,"event_hash":"0xadd7095becdaa725f0f33243630938c861b0bba83dfd217d4055701aa768ec2e","expiration_date":1719133686,"maker_token_price":0,"taker_token_price":0,"maker_usd_amount":0,"taker_usd_amount":0,"state":""},{"exchange":"","order_hash":"52344126666663492359326433927181107201","maker":"0x51c72848c68a965f66fa7a88855f9f7784502a7f","taker":"0x589c86cc1f6043f99222843d397ea4e770841cae","maker_token":"0x6b3595068778dd592e39a122f4f5a5cf09c90fe2","taker_token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","maker_token_amount":"1614402915976888452","taker_token_amount":"3500000000000000","contract_address":"0xbbbbbBB520d69a9775E85b458C58c648259FAD5F","block_number":20153388,"tx_hash":"0xff46ac555ec7da7aa484864dc0df90217b7f46dfa51627c20ef6e25451c64b15","log_index":133,"trade_index":4,"timestamp":1730258461000,"event_hash":"0xadd7095becdaa725f0f33243630938c861b0bba83dfd217d4055701aa768ec2e","expiration_date":1719133686,"maker_token_price":0,"taker_token_price":0,"maker_usd_amount":0,"taker_usd_amount":0,"state":""}]`) diff --git a/v2/pkg/price_filler/utils.go b/v2/pkg/price_filler/utils.go index a2311c4..74829b0 100644 --- a/v2/pkg/price_filler/utils.go +++ b/v2/pkg/price_filler/utils.go @@ -6,18 +6,6 @@ import ( "github.com/KyberNetwork/tradelogs/pkg/convert" ) -var aliasCoinMap = map[string]string{ - "WETH": "ETH", - "STETH": "ETH", -} - -func withAlias(coin string) string { - if s, ok := aliasCoinMap[coin]; ok { - return s - } - return coin -} - // calculateAmountUsd returns raw / (10**decimals) * price func calculateAmountUsd(raw string, decimals int64, price float64) float64 { rawAmt, ok := new(big.Int).SetString(raw, 10) diff --git a/v2/pkg/storage/dashboard/storage.go b/v2/pkg/storage/dashboard/storage.go new file mode 100644 index 0000000..5ef33f5 --- /dev/null +++ b/v2/pkg/storage/dashboard/storage.go @@ -0,0 +1,137 @@ +package dashboard + +import ( + "reflect" + "strings" + + types "github.com/KyberNetwork/tradelogs/v2/pkg/storage/dashboard/types" + "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" +) + +const ( + tokenTable = "token" + makerNameTable = "maker_name" +) + +type Storage struct { + db *sqlx.DB + l *zap.SugaredLogger +} + +func New(l *zap.SugaredLogger, db *sqlx.DB) *Storage { + return &Storage{ + db: db, + l: l, + } +} + +// ----------------------------insert---------------------------- +func (s *Storage) InsertTokens(tokens []types.Token) error { + if len(tokens) == 0 { + return nil + } + + b := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar).Insert(tokenTable).Columns( + types.TokenColumns()..., + ) + for _, token := range tokens { + b = b.Values( + token.SerializeToken()..., + ) + } + q, p, err := b.Suffix(`ON CONFLICT (address, chain_id) DO UPDATE + SET + symbol=excluded.symbol, + decimals=excluded.decimals + `).ToSql() + if err != nil { + s.l.Errorw("Error build insert", "error", err) + return err + } + if _, err := s.db.Exec(q, p...); err != nil { + s.l.Errorw("Error exec insert", "sql", q, "arg", p, "error", err) + return err + } + return nil +} + +func (s *Storage) InsertMakerName(makerName []types.MakerName) error { + if len(makerName) == 0 { + return nil + } + + b := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar).Insert(makerNameTable).Columns( + types.MakerNameColumns()..., + ) + for _, maker := range makerName { + b = b.Values( + maker.SerializeMakerName()..., + ) + } + q, p, err := b.Suffix(`ON CONFLICT (address) DO UPDATE + SET + tag=excluded.tag + `).ToSql() + if err != nil { + s.l.Errorw("Error build insert", "error", err) + return err + } + if _, err := s.db.Exec(q, p...); err != nil { + s.l.Errorw("Error exec insert", "sql", q, "arg", p, "error", err) + return err + } + return nil +} + +// ----------------------------get---------------------------- +func (s *Storage) GetTokens(query types.TokenQuery) ([]types.Token, error) { + builder := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar). + Select(types.TokenColumns()...). + From(tokenTable) + + v := reflect.ValueOf(query) + fields := v.Type() + for i := 0; i < v.NumField(); i++ { + tag := string(fields.Field(i).Tag.Get("form")) + if v.Field(i).IsZero() { + continue + } + if tag == "symbol" { + builder = builder.Where(squirrel.Eq{tag: strings.ToUpper(v.Field(i).String())}) + continue + } + builder = builder.Where(squirrel.Eq{tag: strings.ToLower(v.Field(i).String())}) + } + + q, p, err := builder.ToSql() + if err != nil { + return nil, err + } + + var tokens []types.Token + if err := s.db.Select(&tokens, q, p...); err != nil { + return nil, err + } + + return tokens, nil +} + +func (s *Storage) GetMakerName() ([]types.MakerName, error) { + builder := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar). + Select(types.MakerNameColumns()...). + From(makerNameTable) + + q, p, err := builder.ToSql() + if err != nil { + return nil, err + } + + var makerName []types.MakerName + if err := s.db.Select(&makerName, q, p...); err != nil { + return nil, err + } + + return makerName, nil +} diff --git a/v2/pkg/storage/dashboard/types/maker_name.go b/v2/pkg/storage/dashboard/types/maker_name.go new file mode 100644 index 0000000..17ae798 --- /dev/null +++ b/v2/pkg/storage/dashboard/types/maker_name.go @@ -0,0 +1,22 @@ +package types + +import "strings" + +type MakerName struct { + Address string `db:"address" json:"address"` + Tag string `db:"tag" json:"tag"` +} + +func (o *MakerName) SerializeMakerName() []interface{} { + return []interface{}{ + strings.ToLower(o.Address), + o.Tag, + } +} + +func MakerNameColumns() []string { + return []string{ + "address", + "tag", + } +} diff --git a/v2/pkg/storage/dashboard/types/token.go b/v2/pkg/storage/dashboard/types/token.go new file mode 100644 index 0000000..52aeef6 --- /dev/null +++ b/v2/pkg/storage/dashboard/types/token.go @@ -0,0 +1,37 @@ +package types + +import ( + "strconv" + "strings" +) + +// -------------table------------- +type Token struct { + Address string `db:"address" json:"address,omitempty"` + ChainId int64 `db:"chain_id" json:"chain_id,omitempty"` + Symbol string `db:"symbol" json:"symbol,omitempty"` + Decimals int64 `db:"decimals" json:"decimals,omitempty"` +} + +type TokenQuery struct { + Address string `form:"address" json:"address,omitempty"` + Symbol string `form:"symbol" json:"symbol,omitempty"` +} + +func (o *Token) SerializeToken() []interface{} { + return []interface{}{ + strings.ToLower(o.Address), + strconv.FormatInt(o.ChainId, 10), + strings.ToUpper(o.Symbol), + o.Decimals, + } +} + +func TokenColumns() []string { + return []string{ + "address", + "chain_id", + "symbol", + "decimals", + } +}