Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,6 @@ ethereum_ws_url=wss://ethereum.publicnode.com
# epay
epay_pid=
epay_key=
epay_default_token=usdt
epay_default_currency=cny
epay_default_network=tron
32 changes: 32 additions & 0 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ func GetApiAuthToken() string {
return viper.GetString("api_auth_token")
}

func GetEpaySignKey() string {
key := strings.TrimSpace(GetEpayKey())
if key != "" {
return key
}
return strings.TrimSpace(GetApiAuthToken())
}

func GetRateApiUrl() string {
rateURL := viper.GetString("api_rate_url")
if rateURL == "" {
Expand Down Expand Up @@ -348,3 +356,27 @@ func GetEpayPid() int {
func GetEpayKey() string {
return viper.GetString("epay_key")
}

func GetEpayDefaultToken() string {
token := strings.TrimSpace(viper.GetString("epay_default_token"))
if token == "" {
return "usdt"
}
return strings.ToLower(token)
}

func GetEpayDefaultCurrency() string {
currency := strings.TrimSpace(viper.GetString("epay_default_currency"))
if currency == "" {
return "cny"
}
return strings.ToLower(currency)
}

func GetEpayDefaultNetwork() string {
network := strings.TrimSpace(viper.GetString("epay_default_network"))
if network == "" {
return "tron"
}
return strings.ToLower(network)
}
17 changes: 0 additions & 17 deletions src/controller/comm/order_controller.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package comm

import (
"encoding/json"
"fmt"
"log"

"github.com/assimon/luuu/model/request"
"github.com/assimon/luuu/model/service"
"github.com/assimon/luuu/util/constant"
Expand Down Expand Up @@ -40,35 +36,22 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
if err != nil {
return c.FailJson(ctx, err)
}

jsonBytes, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return c.FailJson(ctx, err)
}

fmt.Printf("switch network response: \n%s", string(jsonBytes))

return c.SucJson(ctx, resp)
}

func (c *BaseCommController) CreateTransactionAndRedirect(ctx echo.Context) (err error) {
req := new(request.CreateTransactionRequest)
if err = ctx.Bind(req); err != nil {
log.Println("bind request error:", err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err = c.ValidateStruct(ctx, req); err != nil {
log.Println("validate request error:", err)
return c.FailJson(ctx, err)
}
resp, err := service.CreateTransaction(req)
if err != nil {
log.Println("create transaction error:", err)
return c.FailJson(ctx, err)
}

fmt.Printf("create transaction response: %+v\n", resp)

tradeID := resp.TradeId

ctx.Redirect(302, "/pay/checkout-counter/"+tradeID)
Expand Down
32 changes: 21 additions & 11 deletions src/controller/comm/pay_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package comm

import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"path/filepath"
Expand All @@ -15,16 +14,33 @@ import (

// CheckoutCounter 收银台
func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
type pageData struct {
response.CheckoutCounterResponse
PaymentOptionsJSON template.JS
}

buildPageData := func(resp response.CheckoutCounterResponse) pageData {
paymentOptionsJSON := template.JS("[]")
if len(resp.PaymentOptions) > 0 {
if b, err := json.Marshal(resp.PaymentOptions); err == nil {
paymentOptionsJSON = template.JS(string(b))
}
}
return pageData{
CheckoutCounterResponse: resp,
PaymentOptionsJSON: paymentOptionsJSON,
}
}

tradeId := ctx.Param("trade_id")
resp, err := service.GetCheckoutCounterByTradeId(tradeId)
if err != nil {
if err == service.ErrOrder {
if err == service.ErrOrderNotFound {
tmpl, err := template.ParseFiles(filepath.Join(config.StaticFilePath, "index.html"))
if err != nil {
return ctx.String(http.StatusOK, err.Error())
}
emptyResp := response.CheckoutCounterResponse{}
return tmpl.Execute(ctx.Response(), emptyResp)
return tmpl.Execute(ctx.Response(), buildPageData(response.CheckoutCounterResponse{}))
}
return ctx.String(http.StatusOK, err.Error())
}
Expand All @@ -33,13 +49,7 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
return ctx.String(http.StatusOK, err.Error())
}

jsonByte, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return ctx.String(http.StatusOK, err.Error())
}
fmt.Printf("%v\n", string(jsonByte))

return tmpl.Execute(ctx.Response(), resp)
return tmpl.Execute(ctx.Response(), buildPageData(*resp))
}

// CheckStatus 支付状态检测
Expand Down
47 changes: 44 additions & 3 deletions src/controller/comm/supported_asset_controller.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package comm

import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/assimon/luuu/config"
"github.com/assimon/luuu/model/data"
"github.com/assimon/luuu/model/response"
"github.com/assimon/luuu/model/service"
"github.com/assimon/luuu/util/constant"
"github.com/assimon/luuu/util/walletaddr"
"github.com/labstack/echo/v4"
"github.com/shopspring/decimal"
)

type addSupportedAssetRequest struct {
Expand All @@ -24,6 +30,19 @@ type updateSupportedAssetRequest struct {

// GetSupportedAssets 对外公开可用链与 token 列表(无需鉴权,仅返回已启用项)。
func (c *BaseCommController) GetSupportedAssets(ctx echo.Context) error {
currency := strings.ToLower(strings.TrimSpace(ctx.QueryParam("currency")))
amountText := strings.TrimSpace(ctx.QueryParam("amount"))
amountFilter := decimal.Zero
hasAmountFilter := false
if amountText != "" {
parsed, err := strconv.ParseFloat(amountText, 64)
if err != nil {
return c.FailJson(ctx, fmt.Errorf("invalid amount: %s", amountText))
}
amountFilter = decimal.NewFromFloat(parsed)
hasAmountFilter = true
}

list, err := data.ListEnabledSupportedAssets()
if err != nil {
return c.FailJson(ctx, err)
Expand All @@ -35,15 +54,37 @@ func (c *BaseCommController) GetSupportedAssets(ctx echo.Context) error {

networkSet := make(map[string]struct{})
for _, w := range wallets {
networkSet[w.Network] = struct{}{}
network := walletaddr.NormalizeNetwork(w.Network)
address := walletaddr.Normalize(network, w.Address)
if !walletaddr.Validate(network, address) {
continue
}
networkSet[network] = struct{}{}
}

grouped := make(map[string][]string)
for _, item := range list {
if _, ok := networkSet[item.Network]; !ok {
network := walletaddr.NormalizeNetwork(item.Network)
token := strings.ToUpper(strings.TrimSpace(item.Token))
if _, ok := networkSet[network]; !ok {
continue
}
if currency != "" {
rate := config.GetRateForCoin(strings.ToLower(token), currency)
if rate <= 0 {
continue
}
if hasAmountFilter {
tokenAmount := amountFilter.Mul(decimal.NewFromFloat(rate))
if tokenAmount.Cmp(decimal.NewFromFloat(service.UsdtMinimumPaymentAmount)) == -1 {
continue
}
}
}
if token == "" {
continue
}
grouped[item.Network] = append(grouped[item.Network], item.Token)
grouped[network] = append(grouped[network], token)
}

networks := make([]string, 0, len(grouped))
Expand Down
13 changes: 12 additions & 1 deletion src/internal/testutil/testdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ func SetupTestDatabases(t testing.TB) func() {
viper.Set("queue_concurrency", 4)
viper.Set("queue_poll_interval_ms", 50)
viper.Set("api_auth_token", "test-token")
viper.Set("epay_key", "test-epay-key")
viper.Set("epay_pid", 1)

config.HTTPAccessLog = false
config.SQLDebug = false
Expand All @@ -38,12 +40,21 @@ func SetupTestDatabases(t testing.TB) func() {
mainDB := mustOpenSQLite(t, filepath.Join(t.TempDir(), "main.db"))
runtimeDB := mustOpenSQLite(t, filepath.Join(t.TempDir(), "runtime.db"))

mustMigrate(t, mainDB, &mdb.Orders{}, &mdb.WalletAddress{})
mustMigrate(t, mainDB, &mdb.Orders{}, &mdb.WalletAddress{}, &mdb.SupportedAsset{})
mustMigrate(t, runtimeDB, &mdb.TransactionLock{})

dao.Mdb = mainDB
dao.RuntimeDB = runtimeDB

if err := mainDB.Create(&[]mdb.SupportedAsset{
{Network: mdb.NetworkTron, Token: "USDT", Status: mdb.TokenStatusEnable},
{Network: mdb.NetworkTron, Token: "TRX", Status: mdb.TokenStatusEnable},
{Network: mdb.NetworkSolana, Token: "USDT", Status: mdb.TokenStatusEnable},
{Network: mdb.NetworkEthereum, Token: "USDT", Status: mdb.TokenStatusEnable},
}).Error; err != nil {
t.Fatalf("seed supported assets: %v", err)
}

return func() {
closeDB(t, runtimeDB)
closeDB(t, mainDB)
Expand Down
69 changes: 56 additions & 13 deletions src/model/data/order_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ func GetSiblingSubOrders(parentTradeId string, excludeTradeId string) ([]mdb.Ord
return orders, err
}

// MarkParentOrderSuccess updates the parent order with the sub-order's payment details.
// Token and network are NOT overwritten — the parent keeps its original values.
// MarkParentOrderSuccess updates the parent order with the actual paid sub-order details.
func MarkParentOrderSuccess(parentTradeId string, sub *mdb.Orders) (bool, error) {
result := dao.Mdb.Model(&mdb.Orders{}).
Where("trade_id = ?", parentTradeId).
Expand All @@ -154,23 +153,67 @@ func MarkParentOrderSuccess(parentTradeId string, sub *mdb.Orders) (bool, error)
"callback_confirm": mdb.CallBackConfirmNo,
"actual_amount": sub.ActualAmount,
"receive_address": sub.ReceiveAddress,
"token": sub.Token,
"network": sub.Network,
})
return result.RowsAffected > 0, result.Error
}

// MarkOrderSelected sets is_selected=true for the given trade_id.
func MarkOrderSelected(tradeId string) error {
return dao.Mdb.Model(&mdb.Orders{}).
Where("trade_id = ?", tradeId).
Update("is_selected", true).Error
func SetSelectedOrder(rootTradeId string, selectedTradeId string) error {
return dao.Mdb.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&mdb.Orders{}).
Where("trade_id = ? OR parent_trade_id = ?", rootTradeId, rootTradeId).
Update("is_selected", false).Error; err != nil {
return err
}
return tx.Model(&mdb.Orders{}).
Where("trade_id = ?", selectedTradeId).
Update("is_selected", true).Error
})
}

// RefreshOrderExpiration resets created_at to now so the expiration timer restarts.
// Called on the parent order when a sub-order is created or returned.
func RefreshOrderExpiration(tradeId string) error {
return dao.Mdb.Model(&mdb.Orders{}).
Where("trade_id = ?", tradeId).
Update("created_at", time.Now()).Error
func GetSelectedOrderInFamily(rootTradeId string) (*mdb.Orders, error) {
order := new(mdb.Orders)
err := dao.Mdb.Model(order).
Where("(trade_id = ? OR parent_trade_id = ?)", rootTradeId, rootTradeId).
Where("status = ?", mdb.StatusWaitPay).
Where("is_selected = ?", true).
Order("parent_trade_id asc, id asc").
Limit(1).
Find(order).Error
return order, err
}

func GetActiveOrdersInFamily(rootTradeId string) ([]mdb.Orders, error) {
var orders []mdb.Orders
err := dao.Mdb.Model(&mdb.Orders{}).
Where("(trade_id = ? OR parent_trade_id = ?)", rootTradeId, rootTradeId).
Where("status = ?", mdb.StatusWaitPay).
Find(&orders).Error
return orders, err
}

func RefreshOrderFamilyExpiration(rootTradeId string, expirationTime time.Duration) error {
now := time.Now()
orders, err := GetActiveOrdersInFamily(rootTradeId)
if err != nil {
return err
}
if len(orders) == 0 {
return nil
}
tradeIDs := make([]string, 0, len(orders))
for _, order := range orders {
tradeIDs = append(tradeIDs, order.TradeId)
}
if err = dao.Mdb.Model(&mdb.Orders{}).
Where("trade_id IN ?", tradeIDs).
Update("created_at", now).Error; err != nil {
return err
}
return dao.RuntimeDB.Model(&mdb.TransactionLock{}).
Where("trade_id IN ?", tradeIDs).
Update("expires_at", now.Add(expirationTime)).Error
}

// ResetCallbackConfirmOk sets callback_confirm back to Ok.
Expand Down
8 changes: 8 additions & 0 deletions src/model/data/supported_asset_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,11 @@ func ListEnabledSupportedAssets() ([]mdb.SupportedAsset, error) {
Find(&list).Error
return list, err
}

func IsSupportedAssetEnabled(network, token string) (bool, error) {
asset, err := GetSupportedAssetByNetworkAndToken(network, token)
if err != nil {
return false, err
}
return asset.ID > 0 && asset.Status == mdb.TokenStatusEnable, nil
}
Loading