feat: get support chain api
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
type NetworkTokenSupport struct {
|
||||||
|
Network string `json:"network"`
|
||||||
|
Tokens []string `json:"tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupportedAssetsResponse struct {
|
||||||
|
Supports []NetworkTokenSupport `json:"supports"`
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+16
-12
@@ -13,21 +13,25 @@ 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 (
|
||||||
SystemErr = Err(400)
|
SystemErr = Err(400)
|
||||||
SignatureErr = Err(401)
|
SignatureErr = Err(401)
|
||||||
WalletAddressAlreadyExists = Err(10001)
|
WalletAddressAlreadyExists = Err(10001)
|
||||||
OrderAlreadyExists = Err(10002)
|
OrderAlreadyExists = Err(10002)
|
||||||
NotAvailableWalletAddress = Err(10003)
|
NotAvailableWalletAddress = Err(10003)
|
||||||
PayAmountErr = Err(10004)
|
PayAmountErr = Err(10004)
|
||||||
NotAvailableAmountErr = Err(10005)
|
NotAvailableAmountErr = Err(10005)
|
||||||
RateAmountErr = Err(10006)
|
RateAmountErr = Err(10006)
|
||||||
OrderBlockAlreadyProcess = Err(10007)
|
OrderBlockAlreadyProcess = Err(10007)
|
||||||
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user