diff --git a/src/controller/comm/supported_asset_controller.go b/src/controller/comm/supported_asset_controller.go new file mode 100644 index 0000000..0a47208 --- /dev/null +++ b/src/controller/comm/supported_asset_controller.go @@ -0,0 +1,135 @@ +package comm + +import ( + "sort" + "strconv" + + "github.com/assimon/luuu/model/data" + "github.com/assimon/luuu/model/response" + "github.com/assimon/luuu/util/constant" + "github.com/labstack/echo/v4" +) + +type addSupportedAssetRequest struct { + Network string `json:"network" validate:"required"` + Token string `json:"token" validate:"required"` + Status int64 `json:"status" validate:"required|in:1,2"` +} + +type updateSupportedAssetRequest struct { + Network string `json:"network" validate:"required"` + Token string `json:"token" validate:"required"` + Status int64 `json:"status" validate:"required|in:1,2"` +} + +// GetSupportedAssets 对外公开可用链与 token 列表(无需鉴权,仅返回已启用项)。 +func (c *BaseCommController) GetSupportedAssets(ctx echo.Context) error { + list, err := data.ListEnabledSupportedAssets() + if err != nil { + return c.FailJson(ctx, err) + } + + grouped := make(map[string][]string) + for _, item := range list { + grouped[item.Network] = append(grouped[item.Network], item.Token) + } + + networks := make([]string, 0, len(grouped)) + for network := range grouped { + networks = append(networks, network) + } + sort.Strings(networks) + + supports := make([]response.NetworkTokenSupport, 0, len(networks)) + for _, network := range networks { + tokens := grouped[network] + sort.Strings(tokens) + supports = append(supports, response.NetworkTokenSupport{ + Network: network, + Tokens: tokens, + }) + } + + return c.SucJson(ctx, response.SupportedAssetsResponse{Supports: supports}) +} + +// ListSupportedAssetRecords 查询支持项明细(无需鉴权)。 +func (c *BaseCommController) ListSupportedAssetRecords(ctx echo.Context) error { + network := ctx.QueryParam("network") + list, err := data.ListSupportedAssets(network) + if err != nil { + return c.FailJson(ctx, err) + } + return c.SucJson(ctx, list) +} + +// GetSupportedAsset 查询单条支持项(无需鉴权)。 +func (c *BaseCommController) GetSupportedAsset(ctx echo.Context) error { + id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) + if err != nil { + return c.FailJson(ctx, constant.ParamsMarshalErr) + } + asset, err := data.GetSupportedAssetByID(id) + if err != nil { + return c.FailJson(ctx, err) + } + if asset.ID <= 0 { + return c.FailJson(ctx, constant.SupportedAssetNotFound) + } + return c.SucJson(ctx, asset) +} + +// AddSupportedAsset 新增支持项(鉴权)。 +func (c *BaseCommController) AddSupportedAsset(ctx echo.Context) error { + req := new(addSupportedAssetRequest) + if err := ctx.Bind(req); err != nil { + return c.FailJson(ctx, constant.ParamsMarshalErr) + } + if err := c.ValidateStruct(ctx, req); err != nil { + return c.FailJson(ctx, err) + } + asset, err := data.AddSupportedAsset(req.Network, req.Token, req.Status) + if err != nil { + return c.FailJson(ctx, err) + } + return c.SucJson(ctx, asset) +} + +// UpdateSupportedAsset 修改支持项(鉴权)。 +func (c *BaseCommController) UpdateSupportedAsset(ctx echo.Context) error { + id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) + if err != nil { + return c.FailJson(ctx, constant.ParamsMarshalErr) + } + req := new(updateSupportedAssetRequest) + if err := ctx.Bind(req); err != nil { + return c.FailJson(ctx, constant.ParamsMarshalErr) + } + if err := c.ValidateStruct(ctx, req); err != nil { + return c.FailJson(ctx, err) + } + asset, err := data.UpdateSupportedAsset(id, req.Network, req.Token, req.Status) + if err != nil { + return c.FailJson(ctx, err) + } + return c.SucJson(ctx, asset) +} + +// DeleteSupportedAsset 删除支持项(鉴权)。 +func (c *BaseCommController) DeleteSupportedAsset(ctx echo.Context) error { + id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) + if err != nil { + return c.FailJson(ctx, constant.ParamsMarshalErr) + } + asset, err := data.GetSupportedAssetByID(id) + if err != nil { + return c.FailJson(ctx, err) + } + if asset.ID <= 0 { + return c.FailJson(ctx, constant.SupportedAssetNotFound) + } + if err := data.DeleteSupportedAssetByID(id); err != nil { + return c.FailJson(ctx, err) + } + return c.SucJson(ctx, nil) +} diff --git a/src/model/dao/mdb_table_init.go b/src/model/dao/mdb_table_init.go index 1082b88..c63bdec 100644 --- a/src/model/dao/mdb_table_init.go +++ b/src/model/dao/mdb_table_init.go @@ -22,5 +22,39 @@ func MdbTableInit() { // panic(err) return } + if err := Mdb.AutoMigrate(&mdb.SupportedAsset{}); err != nil { + color.Red.Printf("[store_db] AutoMigrate DB(SupportedAsset),err=%s\n", err) + return + } + + seedSupportedAssets() }) } + +func seedSupportedAssets() { + var cnt int64 + if err := Mdb.Model(&mdb.SupportedAsset{}).Count(&cnt).Error; err != nil { + color.Red.Printf("[store_db] Count SupportedAsset,err=%s\n", err) + return + } + if cnt > 0 { + return + } + defaults := []mdb.SupportedAsset{ + {Network: mdb.NetworkTron, Token: "TRX", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkTron, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkSolana, Token: "SOL", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkSolana, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkSolana, Token: "USDC", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkEthereum, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkEthereum, Token: "USDC", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkBsc, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkBsc, Token: "USDC", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkPolygon, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkPolygon, Token: "USDC", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkPlasma, Token: "USDT", Status: mdb.TokenStatusEnable}, + } + if err := Mdb.Create(&defaults).Error; err != nil { + color.Red.Printf("[store_db] Seed SupportedAsset,err=%s\n", err) + } +} diff --git a/src/model/data/supported_asset_data.go b/src/model/data/supported_asset_data.go new file mode 100644 index 0000000..5738662 --- /dev/null +++ b/src/model/data/supported_asset_data.go @@ -0,0 +1,99 @@ +package data + +import ( + "strings" + + "github.com/assimon/luuu/model/dao" + "github.com/assimon/luuu/model/mdb" + "github.com/assimon/luuu/util/constant" +) + +func normalizeNetworkToken(network, token string) (string, string) { + return strings.ToLower(strings.TrimSpace(network)), strings.ToUpper(strings.TrimSpace(token)) +} + +func AddSupportedAsset(network, token string, status int64) (*mdb.SupportedAsset, error) { + network, token = normalizeNetworkToken(network, token) + exist, err := GetSupportedAssetByNetworkAndToken(network, token) + if err != nil { + return nil, err + } + if exist.ID > 0 { + return nil, constant.SupportedAssetAlreadyExists + } + asset := &mdb.SupportedAsset{ + Network: network, + Token: token, + Status: status, + } + err = dao.Mdb.Create(asset).Error + return asset, err +} + +func GetSupportedAssetByNetworkAndToken(network, token string) (*mdb.SupportedAsset, error) { + network, token = normalizeNetworkToken(network, token) + asset := new(mdb.SupportedAsset) + err := dao.Mdb.Model(asset). + Where("network = ?", network). + Where("token = ?", token). + Limit(1).Find(asset).Error + return asset, err +} + +func GetSupportedAssetByID(id uint64) (*mdb.SupportedAsset, error) { + asset := new(mdb.SupportedAsset) + err := dao.Mdb.Model(asset).Limit(1).Find(asset, id).Error + return asset, err +} + +func UpdateSupportedAsset(id uint64, network, token string, status int64) (*mdb.SupportedAsset, error) { + network, token = normalizeNetworkToken(network, token) + + asset, err := GetSupportedAssetByID(id) + if err != nil { + return nil, err + } + if asset.ID <= 0 { + return nil, constant.SupportedAssetNotFound + } + + exist, err := GetSupportedAssetByNetworkAndToken(network, token) + if err != nil { + return nil, err + } + if exist.ID > 0 && exist.ID != id { + return nil, constant.SupportedAssetAlreadyExists + } + + asset.Network = network + asset.Token = token + asset.Status = status + if err := dao.Mdb.Save(asset).Error; err != nil { + return nil, err + } + return asset, nil +} + +func DeleteSupportedAssetByID(id uint64) error { + return dao.Mdb.Where("id = ?", id).Delete(&mdb.SupportedAsset{}).Error +} + +func ListSupportedAssets(network string) ([]mdb.SupportedAsset, error) { + network = strings.ToLower(strings.TrimSpace(network)) + var list []mdb.SupportedAsset + tx := dao.Mdb.Model(&mdb.SupportedAsset{}) + if network != "" { + tx = tx.Where("network = ?", network) + } + err := tx.Order("network asc, token asc").Find(&list).Error + return list, err +} + +func ListEnabledSupportedAssets() ([]mdb.SupportedAsset, error) { + var list []mdb.SupportedAsset + err := dao.Mdb.Model(&mdb.SupportedAsset{}). + Where("status = ?", mdb.TokenStatusEnable). + Order("network asc, token asc"). + Find(&list).Error + return list, err +} diff --git a/src/model/mdb/supported_asset_mdb.go b/src/model/mdb/supported_asset_mdb.go new file mode 100644 index 0000000..1698a67 --- /dev/null +++ b/src/model/mdb/supported_asset_mdb.go @@ -0,0 +1,12 @@ +package mdb + +type SupportedAsset struct { + Network string `gorm:"column:network;uniqueIndex:supported_asset_network_token_uindex,priority:1" json:"network"` + Token string `gorm:"column:token;uniqueIndex:supported_asset_network_token_uindex,priority:2" json:"token"` + Status int64 `gorm:"column:status;default:1" json:"status"` + BaseModel +} + +func (s *SupportedAsset) TableName() string { + return "supported_asset" +} diff --git a/src/model/response/support_response.go b/src/model/response/support_response.go new file mode 100644 index 0000000..b78855d --- /dev/null +++ b/src/model/response/support_response.go @@ -0,0 +1,10 @@ +package response + +type NetworkTokenSupport struct { + Network string `json:"network"` + Tokens []string `json:"tokens"` +} + +type SupportedAssetsResponse struct { + Supports []NetworkTokenSupport `json:"supports"` +} diff --git a/src/route/router.go b/src/route/router.go index 8ee23db..4170df8 100644 --- a/src/route/router.go +++ b/src/route/router.go @@ -56,6 +56,12 @@ func RegisterRoute(e *echo.Echo) { // gmpay v1 routes gmpayV1 := paymentRoute.Group("/gmpay/v1") gmpayV1.POST("/order/create-transaction", comm.Ctrl.CreateTransaction, middleware.CheckApiSign()) + gmpayV1.GET("/supported-assets", comm.Ctrl.GetSupportedAssets) + gmpayV1.GET("/supported-assets/records", comm.Ctrl.ListSupportedAssetRecords) + gmpayV1.GET("/supported-assets/:id", comm.Ctrl.GetSupportedAsset) + gmpayV1.POST("/supported-assets/add", comm.Ctrl.AddSupportedAsset, middleware.CheckApiToken()) + gmpayV1.POST("/supported-assets/:id/update", comm.Ctrl.UpdateSupportedAsset, middleware.CheckApiToken()) + gmpayV1.POST("/supported-assets/:id/delete", comm.Ctrl.DeleteSupportedAsset, middleware.CheckApiToken()) // wallet management routes walletV1 := gmpayV1.Group("/wallet", middleware.CheckApiToken()) diff --git a/src/route/router_test.go b/src/route/router_test.go index ddc0a5c..b6ee09c 100644 --- a/src/route/router_test.go +++ b/src/route/router_test.go @@ -52,11 +52,30 @@ func setupTestEnv(t *testing.T) *echo.Echo { } // ensure tables exist (MdbTableInit uses sync.Once, so migrate directly) - dao.Mdb.AutoMigrate(&mdb.Orders{}, &mdb.WalletAddress{}) + dao.Mdb.AutoMigrate(&mdb.Orders{}, &mdb.WalletAddress{}, &mdb.SupportedAsset{}) // seed wallet addresses dao.Mdb.Create(&mdb.WalletAddress{Network: mdb.NetworkTron, Address: "TTestTronAddress001", Status: mdb.TokenStatusEnable}) dao.Mdb.Create(&mdb.WalletAddress{Network: mdb.NetworkSolana, Address: "SolTestAddress001", Status: mdb.TokenStatusEnable}) + // seed supported assets if empty + var supportCnt int64 + dao.Mdb.Model(&mdb.SupportedAsset{}).Count(&supportCnt) + if supportCnt == 0 { + dao.Mdb.Create(&[]mdb.SupportedAsset{ + {Network: mdb.NetworkTron, Token: "TRX", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkTron, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkSolana, Token: "SOL", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkSolana, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkSolana, Token: "USDC", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkEthereum, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkEthereum, Token: "USDC", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkBsc, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkBsc, Token: "USDC", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkPolygon, Token: "USDT", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkPolygon, Token: "USDC", Status: mdb.TokenStatusEnable}, + {Network: mdb.NetworkPlasma, Token: "USDT", Status: mdb.TokenStatusEnable}, + }) + } e := echo.New() RegisterRoute(e) @@ -207,6 +226,104 @@ func parseResp(t *testing.T, rec *httptest.ResponseRecorder) map[string]interfac return resp } +func TestGetSupportedAssetsPublic(t *testing.T) { + e := setupTestEnv(t) + + req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/supported-assets", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d, body=%s", rec.Code, rec.Body.String()) + } + + resp := parseResp(t, rec) + if resp["status_code"].(float64) != 200 { + t.Fatalf("expected status_code=200, got %v", resp["status_code"]) + } + + data, ok := resp["data"].(map[string]interface{}) + if !ok { + t.Fatalf("expected object data, got %T", resp["data"]) + } + supports, ok := data["supports"].([]interface{}) + if !ok { + t.Fatalf("expected supports array, got %T", data["supports"]) + } + if len(supports) < 6 { + t.Fatalf("expected >= 6 network supports, got %d", len(supports)) + } + + seen := map[string]bool{} + for _, item := range supports { + row := item.(map[string]interface{}) + network := row["network"].(string) + seen[network] = true + } + for _, n := range []string{"tron", "solana", "eth", "bsc", "polygon", "plasma"} { + if !seen[n] { + t.Fatalf("missing network support: %s", n) + } + } +} + +func TestSupportedAssetCRUD(t *testing.T) { + e := setupTestEnv(t) + + // public query list (no auth) + req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/supported-assets/records", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + resp := parseResp(t, rec) + if resp["status_code"].(float64) != 200 { + t.Fatalf("public list failed: %v", resp) + } + + // create requires auth + rec = doPostWithToken(e, "/payments/gmpay/v1/supported-assets/add", map[string]interface{}{ + "network": "arb", + "token": "usdt", + "status": 1, + }) + resp = parseResp(t, rec) + if resp["status_code"].(float64) != 200 { + t.Fatalf("add supported asset failed: %v", resp) + } + created := resp["data"].(map[string]interface{}) + assetID := fmt.Sprintf("%.0f", created["id"].(float64)) + + // get by id is public + req = httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/supported-assets/"+assetID, nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + resp = parseResp(t, rec) + if resp["status_code"].(float64) != 200 { + t.Fatalf("public get by id failed: %v", resp) + } + + // update requires auth + rec = doPostWithToken(e, "/payments/gmpay/v1/supported-assets/"+assetID+"/update", map[string]interface{}{ + "network": "arbitrum", + "token": "usdc", + "status": 1, + }) + resp = parseResp(t, rec) + if resp["status_code"].(float64) != 200 { + t.Fatalf("update supported asset failed: %v", resp) + } + updated := resp["data"].(map[string]interface{}) + if updated["network"] != "arbitrum" || updated["token"] != "USDC" { + t.Fatalf("unexpected updated data: %v", updated) + } + + // delete requires auth + rec = doPostWithToken(e, "/payments/gmpay/v1/supported-assets/"+assetID+"/delete", nil) + resp = parseResp(t, rec) + if resp["status_code"].(float64) != 200 { + t.Fatalf("delete supported asset failed: %v", resp) + } +} + // TestWalletAddAndList tests adding wallets via API and listing them. func TestWalletAddAndList(t *testing.T) { e := setupTestEnv(t) diff --git a/src/task/listen.go b/src/task/listen.go index a05177c..67ccf21 100644 --- a/src/task/listen.go +++ b/src/task/listen.go @@ -9,8 +9,8 @@ func Start() { log.Sugar.Info("[task] Starting task scheduler...") go StartEthereumWebSocketListener() go StartBscWebSocketListener() - go StartPolygonWebSocketListener() - go StartPlasmaWebSocketListener() + //go StartPolygonWebSocketListener() + //go StartPlasmaWebSocketListener() c := cron.New() // trc20钱包监听 diff --git a/src/telegram/handle.go b/src/telegram/handle.go index eb83080..95522df 100644 --- a/src/telegram/handle.go +++ b/src/telegram/handle.go @@ -2,6 +2,7 @@ package telegram import ( "fmt" + "sort" "strings" "sync" "time" @@ -14,12 +15,14 @@ import ( ) const ( - ReplayAddWallet = "请发给我 TRON(T 开头)或以太坊主网(0x 开头)收款地址" + ReplaySelectNetwork = "请选择要添加的钱包网络" + ReplayAddWallet = "请发送 %s 网络收款地址" pendingWalletAddressTTL = 5 * time.Minute ) type pendingWalletAddressState struct { RequestedAt time.Time + Network string } var pendingWalletAddressUsers sync.Map @@ -36,7 +39,7 @@ func OnTextMessageHandle(c tb.Context) error { senderID = sender.ID } - isReplyFlow := msg.ReplyTo != nil && msg.ReplyTo.Text == ReplayAddWallet + isReplyFlow := msg.ReplyTo != nil && strings.HasPrefix(msg.ReplyTo.Text, "请发送 ") isPendingFlow := isWalletAddressPending(senderID) if !isReplyFlow && !isPendingFlow { return nil @@ -47,22 +50,25 @@ func OnTextMessageHandle(c tb.Context) error { } msgText := strings.TrimSpace(msg.Text) - var err error - switch { - case isValidTronAddress(msgText): - _, err = data.AddWalletAddress(msgText) - case isValidEthereumAddress(msgText): - _, err = data.AddWalletAddressWithNetwork(mdb.NetworkEthereum, strings.ToLower(msgText)) - default: - _ = c.Send(fmt.Sprintf("钱包 [%s] 添加失败:不是合法的 TRON 或以太坊地址", msgText)) + state, _ := getPendingWalletAddressState(senderID) + if state.Network == "" { + _ = c.Send("请先选择网络,再发送地址。") return nil } + + var err error + if !isValidAddressByNetwork(state.Network, msgText) { + _ = c.Send(fmt.Sprintf("钱包 [%s] 添加失败:不是合法的 %s 地址", msgText, strings.ToUpper(state.Network))) + return nil + } + storeAddress := normalizeWalletAddressByNetwork(state.Network, msgText) + _, err = data.AddWalletAddressWithNetwork(state.Network, storeAddress) if err != nil { return c.Send(err.Error()) } pendingWalletAddressUsers.Delete(senderID) - _ = c.Send(fmt.Sprintf("钱包 [%s] 添加成功", msgText)) + _ = c.Send(fmt.Sprintf("钱包 [%s] 添加成功(%s)", storeAddress, strings.ToUpper(state.Network))) return WalletList(c) } @@ -94,11 +100,26 @@ func WalletList(c tb.Context) error { addBtn := tb.InlineButton{Text: "添加钱包地址", Unique: "AddWallet"} bots.Handle(&addBtn, func(c tb.Context) error { - if sender := c.Sender(); sender != nil { - pendingWalletAddressUsers.Store(sender.ID, pendingWalletAddressState{RequestedAt: time.Now()}) + chains, err := getEnabledSupportedNetworks() + if err != nil { + return c.Send("读取支持链失败:" + err.Error()) } - return c.Send(ReplayAddWallet, &tb.ReplyMarkup{ - ForceReply: true, + if len(chains) == 0 { + return c.Send("当前没有可用链,请先在后台配置 supported-assets。") + } + + rows := make([][]tb.InlineButton, 0, len(chains)) + for _, network := range chains { + btn := tb.InlineButton{ + Text: strings.ToUpper(network), + Unique: "SelectWalletNetwork", + Data: network, + } + bots.Handle(&btn, SelectWalletNetwork) + rows = append(rows, []tb.InlineButton{btn}) + } + return c.EditOrSend(ReplaySelectNetwork, &tb.ReplyMarkup{ + InlineKeyboard: rows, }) }) btnList = append(btnList, []tb.InlineButton{addBtn}) @@ -108,6 +129,22 @@ func WalletList(c tb.Context) error { }) } +func SelectWalletNetwork(c tb.Context) error { + network := strings.ToLower(strings.TrimSpace(c.Data())) + if network == "" { + return c.Send("请选择有效网络") + } + if sender := c.Sender(); sender != nil { + pendingWalletAddressUsers.Store(sender.ID, pendingWalletAddressState{ + RequestedAt: time.Now(), + Network: network, + }) + } + return c.Send(fmt.Sprintf(ReplayAddWallet, strings.ToUpper(network)), &tb.ReplyMarkup{ + ForceReply: true, + }) +} + func WalletInfo(c tb.Context) error { id := mathutil.MustUint(c.Data()) tokenInfo, err := data.GetWalletAddressById(id) @@ -209,3 +246,33 @@ func isWalletAddressPending(userID int64) bool { } return true } + +func getPendingWalletAddressState(userID int64) (pendingWalletAddressState, bool) { + v, ok := pendingWalletAddressUsers.Load(userID) + if !ok { + return pendingWalletAddressState{}, false + } + state, ok := v.(pendingWalletAddressState) + return state, ok +} + +func getEnabledSupportedNetworks() ([]string, error) { + assets, err := data.ListEnabledSupportedAssets() + if err != nil { + return nil, err + } + set := make(map[string]struct{}) + for _, a := range assets { + n := strings.ToLower(strings.TrimSpace(a.Network)) + if n == "" { + continue + } + set[n] = struct{}{} + } + networks := make([]string, 0, len(set)) + for n := range set { + networks = append(networks, n) + } + sort.Strings(networks) + return networks, nil +} diff --git a/src/telegram/utils.go b/src/telegram/utils.go index 114a840..c08aeb5 100644 --- a/src/telegram/utils.go +++ b/src/telegram/utils.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "strings" + "github.com/assimon/luuu/model/mdb" "github.com/btcsuite/btcutil/base58" ) @@ -44,3 +45,34 @@ func isValidTronAddress(addr string) bool { return string(checksum) == string(hash2[:4]) } + +func isValidSolanaAddress(addr string) bool { + addr = strings.TrimSpace(addr) + if addr == "" { + return false + } + decoded := base58.Decode(addr) + return len(decoded) == 32 +} + +func isValidAddressByNetwork(network, addr string) bool { + switch strings.ToLower(strings.TrimSpace(network)) { + case mdb.NetworkTron: + return isValidTronAddress(addr) + case mdb.NetworkSolana: + return isValidSolanaAddress(addr) + default: + // 其余 EVM 链统一使用 0x 地址校验 + return isValidEthereumAddress(addr) + } +} + +func normalizeWalletAddressByNetwork(network, addr string) string { + addr = strings.TrimSpace(addr) + switch strings.ToLower(strings.TrimSpace(network)) { + case mdb.NetworkTron, mdb.NetworkSolana: + return addr + default: + return strings.ToLower(addr) + } +} diff --git a/src/util/constant/errno.go b/src/util/constant/errno.go index 372f676..a94ec79 100644 --- a/src/util/constant/errno.go +++ b/src/util/constant/errno.go @@ -13,21 +13,25 @@ var Errno = map[int]string{ 10008: "order does not exist", 10009: "failed to parse request params", 10010: "order status already changed", + 10011: "supported asset already exists", + 10012: "supported asset not found", } var ( - SystemErr = Err(400) - SignatureErr = Err(401) - WalletAddressAlreadyExists = Err(10001) - OrderAlreadyExists = Err(10002) - NotAvailableWalletAddress = Err(10003) - PayAmountErr = Err(10004) - NotAvailableAmountErr = Err(10005) - RateAmountErr = Err(10006) - OrderBlockAlreadyProcess = Err(10007) - OrderNotExists = Err(10008) - ParamsMarshalErr = Err(10009) - OrderStatusConflict = Err(10010) + SystemErr = Err(400) + SignatureErr = Err(401) + WalletAddressAlreadyExists = Err(10001) + OrderAlreadyExists = Err(10002) + NotAvailableWalletAddress = Err(10003) + PayAmountErr = Err(10004) + NotAvailableAmountErr = Err(10005) + RateAmountErr = Err(10006) + OrderBlockAlreadyProcess = Err(10007) + OrderNotExists = Err(10008) + ParamsMarshalErr = Err(10009) + OrderStatusConflict = Err(10010) + SupportedAssetAlreadyExists = Err(10011) + SupportedAssetNotFound = Err(10012) ) type RspError struct {