feat: get support chain api

This commit is contained in:
Ginta
2026-04-14 18:57:38 +08:00
parent 5e68bf8309
commit 1e2282ba2e
11 changed files with 546 additions and 30 deletions
@@ -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)
}
+34
View File
@@ -22,5 +22,39 @@ func MdbTableInit() {
// panic(err) // panic(err)
return 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)
}
}
+99
View File
@@ -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
}
+12
View File
@@ -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"
}
+10
View File
@@ -0,0 +1,10 @@
package response
type NetworkTokenSupport struct {
Network string `json:"network"`
Tokens []string `json:"tokens"`
}
type SupportedAssetsResponse struct {
Supports []NetworkTokenSupport `json:"supports"`
}
+6
View File
@@ -56,6 +56,12 @@ func RegisterRoute(e *echo.Echo) {
// gmpay v1 routes // gmpay v1 routes
gmpayV1 := paymentRoute.Group("/gmpay/v1") gmpayV1 := paymentRoute.Group("/gmpay/v1")
gmpayV1.POST("/order/create-transaction", comm.Ctrl.CreateTransaction, middleware.CheckApiSign()) 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 // wallet management routes
walletV1 := gmpayV1.Group("/wallet", middleware.CheckApiToken()) walletV1 := gmpayV1.Group("/wallet", middleware.CheckApiToken())
+118 -1
View File
@@ -52,11 +52,30 @@ func setupTestEnv(t *testing.T) *echo.Echo {
} }
// ensure tables exist (MdbTableInit uses sync.Once, so migrate directly) // 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 // seed wallet addresses
dao.Mdb.Create(&mdb.WalletAddress{Network: mdb.NetworkTron, Address: "TTestTronAddress001", Status: mdb.TokenStatusEnable}) 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}) 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() e := echo.New()
RegisterRoute(e) RegisterRoute(e)
@@ -207,6 +226,104 @@ func parseResp(t *testing.T, rec *httptest.ResponseRecorder) map[string]interfac
return resp 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. // TestWalletAddAndList tests adding wallets via API and listing them.
func TestWalletAddAndList(t *testing.T) { func TestWalletAddAndList(t *testing.T) {
e := setupTestEnv(t) e := setupTestEnv(t)
+2 -2
View File
@@ -9,8 +9,8 @@ func Start() {
log.Sugar.Info("[task] Starting task scheduler...") log.Sugar.Info("[task] Starting task scheduler...")
go StartEthereumWebSocketListener() go StartEthereumWebSocketListener()
go StartBscWebSocketListener() go StartBscWebSocketListener()
go StartPolygonWebSocketListener() //go StartPolygonWebSocketListener()
go StartPlasmaWebSocketListener() //go StartPlasmaWebSocketListener()
c := cron.New() c := cron.New()
// trc20钱包监听 // trc20钱包监听
+82 -15
View File
@@ -2,6 +2,7 @@ package telegram
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -14,12 +15,14 @@ import (
) )
const ( const (
ReplayAddWallet = "请发给我 TRON(T 开头)或以太坊主网(0x 开头)收款地址" ReplaySelectNetwork = "请选择要添加的钱包网络"
ReplayAddWallet = "请发送 %s 网络收款地址"
pendingWalletAddressTTL = 5 * time.Minute pendingWalletAddressTTL = 5 * time.Minute
) )
type pendingWalletAddressState struct { type pendingWalletAddressState struct {
RequestedAt time.Time RequestedAt time.Time
Network string
} }
var pendingWalletAddressUsers sync.Map var pendingWalletAddressUsers sync.Map
@@ -36,7 +39,7 @@ func OnTextMessageHandle(c tb.Context) error {
senderID = sender.ID senderID = sender.ID
} }
isReplyFlow := msg.ReplyTo != nil && msg.ReplyTo.Text == ReplayAddWallet isReplyFlow := msg.ReplyTo != nil && strings.HasPrefix(msg.ReplyTo.Text, "请发送 ")
isPendingFlow := isWalletAddressPending(senderID) isPendingFlow := isWalletAddressPending(senderID)
if !isReplyFlow && !isPendingFlow { if !isReplyFlow && !isPendingFlow {
return nil return nil
@@ -47,22 +50,25 @@ func OnTextMessageHandle(c tb.Context) error {
} }
msgText := strings.TrimSpace(msg.Text) msgText := strings.TrimSpace(msg.Text)
var err error state, _ := getPendingWalletAddressState(senderID)
switch { if state.Network == "" {
case isValidTronAddress(msgText): _ = c.Send("请先选择网络,再发送地址。")
_, err = data.AddWalletAddress(msgText)
case isValidEthereumAddress(msgText):
_, err = data.AddWalletAddressWithNetwork(mdb.NetworkEthereum, strings.ToLower(msgText))
default:
_ = c.Send(fmt.Sprintf("钱包 [%s] 添加失败:不是合法的 TRON 或以太坊地址", msgText))
return nil 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 { if err != nil {
return c.Send(err.Error()) return c.Send(err.Error())
} }
pendingWalletAddressUsers.Delete(senderID) pendingWalletAddressUsers.Delete(senderID)
_ = c.Send(fmt.Sprintf("钱包 [%s] 添加成功", msgText)) _ = c.Send(fmt.Sprintf("钱包 [%s] 添加成功%s", storeAddress, strings.ToUpper(state.Network)))
return WalletList(c) return WalletList(c)
} }
@@ -94,11 +100,26 @@ func WalletList(c tb.Context) error {
addBtn := tb.InlineButton{Text: "添加钱包地址", Unique: "AddWallet"} addBtn := tb.InlineButton{Text: "添加钱包地址", Unique: "AddWallet"}
bots.Handle(&addBtn, func(c tb.Context) error { bots.Handle(&addBtn, func(c tb.Context) error {
if sender := c.Sender(); sender != nil { chains, err := getEnabledSupportedNetworks()
pendingWalletAddressUsers.Store(sender.ID, pendingWalletAddressState{RequestedAt: time.Now()}) if err != nil {
return c.Send("读取支持链失败:" + err.Error())
} }
return c.Send(ReplayAddWallet, &tb.ReplyMarkup{ if len(chains) == 0 {
ForceReply: true, 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}) 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 { func WalletInfo(c tb.Context) error {
id := mathutil.MustUint(c.Data()) id := mathutil.MustUint(c.Data())
tokenInfo, err := data.GetWalletAddressById(id) tokenInfo, err := data.GetWalletAddressById(id)
@@ -209,3 +246,33 @@ func isWalletAddressPending(userID int64) bool {
} }
return true 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
}
+32
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"strings" "strings"
"github.com/assimon/luuu/model/mdb"
"github.com/btcsuite/btcutil/base58" "github.com/btcsuite/btcutil/base58"
) )
@@ -44,3 +45,34 @@ func isValidTronAddress(addr string) bool {
return string(checksum) == string(hash2[:4]) 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)
}
}
+4
View File
@@ -13,6 +13,8 @@ var Errno = map[int]string{
10008: "order does not exist", 10008: "order does not exist",
10009: "failed to parse request params", 10009: "failed to parse request params",
10010: "order status already changed", 10010: "order status already changed",
10011: "supported asset already exists",
10012: "supported asset not found",
} }
var ( var (
@@ -28,6 +30,8 @@ var (
OrderNotExists = Err(10008) OrderNotExists = Err(10008)
ParamsMarshalErr = Err(10009) ParamsMarshalErr = Err(10009)
OrderStatusConflict = Err(10010) OrderStatusConflict = Err(10010)
SupportedAssetAlreadyExists = Err(10011)
SupportedAssetNotFound = Err(10012)
) )
type RspError struct { type RspError struct {