mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 10:16:15 +00:00
@@ -15,10 +15,10 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/assimon/epusdt/stargazers"><img src="https://img.shields.io/github/stars/assimon/epusdt?style=flat-square&color=f5c542" alt="GitHub Stars 3000+"></a>
|
<a href="https://github.com/GMWalletApp/epusdt/stargazers"><img src="https://img.shields.io/github/stars/GMWalletApp/epusdt?style=flat-square&color=f5c542" alt="GitHub Stars 3000+"></a>
|
||||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/badge/License-GPLv3-blue?style=flat-square" alt="GPLv3 License"></a>
|
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/badge/License-GPLv3-blue?style=flat-square" alt="GPLv3 License"></a>
|
||||||
<a href="https://golang.org"><img src="https://img.shields.io/badge/Go-1.16+-00ADD8?style=flat-square&logo=go&logoColor=white" alt="Go 1.16+"></a>
|
<a href="https://golang.org"><img src="https://img.shields.io/badge/Go-1.16+-00ADD8?style=flat-square&logo=go&logoColor=white" alt="Go 1.16+"></a>
|
||||||
<a href="https://github.com/assimon/epusdt/releases"><img src="https://img.shields.io/github/v/release/assimon/epusdt?style=flat-square&color=green" alt="Latest Release"></a>
|
<a href="https://github.com/GMWalletApp/epusdt/releases"><img src="https://img.shields.io/github/v/release/GMWalletApp/epusdt?style=flat-square&color=green" alt="Latest Release"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -42,7 +42,7 @@ Deploy it privately, integrate via HTTP API, and start receiving **crypto paymen
|
|||||||
| **Polygon** | USDT、USDC |
|
| **Polygon** | USDT、USDC |
|
||||||
| **更多** | 持续扩展中… |
|
| **更多** | 持续扩展中… |
|
||||||
|
|
||||||
> 💡 具体支持的链与代币以 [最新版本](https://github.com/assimon/epusdt/releases) 及 [官方文档](https://epusdt.com) 为准。
|
> 💡 具体支持的链与代币以 [最新版本](https://github.com/GMWalletApp/epusdt/releases) 及 [官方文档](https://epusdt.com) 为准。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ Epusdt 通过监听多条区块链网络(TRC20、ERC20、BEP20、Polygon 等
|
|||||||
|
|
||||||
## 💬 社区与支持
|
## 💬 社区与支持
|
||||||
|
|
||||||
**遇到问题?** 请优先在 GitHub 提交 [Issue](https://github.com/assimon/epusdt/issues),我们会**优先处理** Issue 中的反馈。
|
**遇到问题?** 请优先在 GitHub 提交 [Issue](https://github.com/GMWalletApp/epusdt/issues),我们会**优先处理** Issue 中的反馈。
|
||||||
|
|
||||||
加入 Telegram 社区,获取最新开发动态,参与需求调研,与超过5000名活跃用户交流使用经验,对接商业资源:
|
加入 Telegram 社区,获取最新开发动态,参与需求调研,与超过5000名活跃用户交流使用经验,对接商业资源:
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* The file was created by Assimon.
|
* The file was created by Assimon.
|
||||||
*
|
*
|
||||||
* @author assimon<ashang@utf8.hk>
|
* @author GMWalletApp<ashang@utf8.hk>
|
||||||
* @copyright assimon<ashang@utf8.hk>
|
* @copyright GMWalletApp<ashang@utf8.hk>
|
||||||
* @link http://utf8.hk/
|
* @link http://utf8.hk/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* The file was created by Assimon.
|
* The file was created by Assimon.
|
||||||
*
|
*
|
||||||
* @author assimon<ashang@utf8.hk>
|
* @author GMWalletApp<ashang@utf8.hk>
|
||||||
* @copyright assimon<ashang@utf8.hk>
|
* @copyright GMWalletApp<ashang@utf8.hk>
|
||||||
* @link http://utf8.hk/
|
* @link http://utf8.hk/
|
||||||
*/
|
*/
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|||||||
+11
-11
@@ -3,14 +3,14 @@ package bootstrap
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/mq"
|
"github.com/GMWalletApp/epusdt/mq"
|
||||||
"github.com/assimon/luuu/task"
|
"github.com/GMWalletApp/epusdt/task"
|
||||||
"github.com/assimon/luuu/telegram"
|
"github.com/GMWalletApp/epusdt/telegram"
|
||||||
appjwt "github.com/assimon/luuu/util/jwt"
|
appjwt "github.com/GMWalletApp/epusdt/util/jwt"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,15 +41,15 @@ func InitApp() {
|
|||||||
config.RateApiUrl = config.GetRateApiUrl()
|
config.RateApiUrl = config.GetRateApiUrl()
|
||||||
// Seed admin account and JWT secret so the management console is
|
// Seed admin account and JWT secret so the management console is
|
||||||
// immediately usable on a fresh install. Both are idempotent.
|
// immediately usable on a fresh install. Both are idempotent.
|
||||||
adminPwd, isNew, err := data.EnsureDefaultAdmin()
|
_, isNew, err := data.EnsureDefaultAdmin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
color.Red.Printf("[bootstrap] ensure default admin err=%s\n", err)
|
color.Red.Printf("[bootstrap] ensure default admin err=%s\n", err)
|
||||||
}
|
}
|
||||||
if isNew {
|
if isNew {
|
||||||
color.Yellow.Println("╔════════════════════════════════════════════════════════════════════════╗")
|
color.Yellow.Println("╔════════════════════════════════════════════════════════════════════════╗")
|
||||||
color.Yellow.Println("║ Default admin account created. Change the password via admin console! ║")
|
color.Yellow.Println("║ Default admin account created. Fetch one-time password via API first!║")
|
||||||
color.Yellow.Printf("║ Username: admin ║\n")
|
color.Yellow.Printf("║ Username: admin ║\n")
|
||||||
color.Yellow.Printf("║ Password: %-60s ║\n", adminPwd)
|
color.Yellow.Println("║ GET /admin/api/v1/auth/init-password (one-time) ║")
|
||||||
color.Yellow.Println("╚════════════════════════════════════════════════════════════════════════╝")
|
color.Yellow.Println("╚════════════════════════════════════════════════════════════════════════╝")
|
||||||
}
|
}
|
||||||
if _, err := appjwt.EnsureSecret(); err != nil {
|
if _, err := appjwt.EnsureSecret(); err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/bootstrap"
|
"github.com/GMWalletApp/epusdt/bootstrap"
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/install"
|
"github.com/GMWalletApp/epusdt/install"
|
||||||
"github.com/assimon/luuu/middleware"
|
"github.com/GMWalletApp/epusdt/middleware"
|
||||||
"github.com/assimon/luuu/route"
|
"github.com/GMWalletApp/epusdt/route"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
luluHttp "github.com/assimon/luuu/util/http"
|
luluHttp "github.com/GMWalletApp/epusdt/util/http"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -72,6 +73,15 @@ func HttpServerStart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.Use(echoMiddleware.StaticWithConfig(echoMiddleware.StaticConfig{
|
e.Use(echoMiddleware.StaticWithConfig(echoMiddleware.StaticConfig{
|
||||||
|
Skipper: func(c echo.Context) bool {
|
||||||
|
path := c.Request().URL.Path
|
||||||
|
if path == "/install" || strings.HasPrefix(path, "/install/") {
|
||||||
|
// The install wizard is only served by install.RunInstallServer
|
||||||
|
// before bootstrap. Once main server starts, block /install.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return luluHttp.ShouldSkipSPAFallback(path)
|
||||||
|
},
|
||||||
HTML5: true,
|
HTML5: true,
|
||||||
Index: "index.html",
|
Index: "index.html",
|
||||||
Root: wwwRoot,
|
Root: wwwRoot,
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package command
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
-15
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/util/http_client"
|
"github.com/GMWalletApp/epusdt/util/http_client"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
@@ -36,7 +36,6 @@ var (
|
|||||||
TgManage int64
|
TgManage int64
|
||||||
UsdtRate float64
|
UsdtRate float64
|
||||||
RateApiUrl string
|
RateApiUrl string
|
||||||
TRON_GRID_API_KEY string
|
|
||||||
BuildVersion = "0.0.0-dev"
|
BuildVersion = "0.0.0-dev"
|
||||||
BuildCommit = "none"
|
BuildCommit = "none"
|
||||||
BuildDate = "unknown"
|
BuildDate = "unknown"
|
||||||
@@ -79,7 +78,6 @@ func Init() {
|
|||||||
TgManage = viper.GetInt64("tg_manage")
|
TgManage = viper.GetInt64("tg_manage")
|
||||||
|
|
||||||
RateApiUrl = GetRateApiUrl()
|
RateApiUrl = GetRateApiUrl()
|
||||||
TRON_GRID_API_KEY = viper.GetString("tron_grid_api_key")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustMkdir(path string) {
|
func mustMkdir(path string) {
|
||||||
@@ -398,15 +396,3 @@ func GetCallbackRetryBaseDuration() time.Duration {
|
|||||||
}
|
}
|
||||||
return time.Duration(seconds) * time.Second
|
return time.Duration(seconds) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSolanaRpcUrl() string {
|
|
||||||
rpcUrl := viper.GetString("solana_rpc_url")
|
|
||||||
if rpcUrl == "" {
|
|
||||||
return "https://api.mainnet-beta.solana.com"
|
|
||||||
}
|
|
||||||
return rpcUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetEthereumWsUrl() string {
|
|
||||||
return strings.TrimSpace(viper.GetString("ethereum_ws_url"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
appjwt "github.com/assimon/luuu/util/jwt"
|
appjwt "github.com/GMWalletApp/epusdt/util/jwt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,6 +34,12 @@ type MeResponse struct {
|
|||||||
PasswordIsDefault bool `json:"password_is_default" example:"true"`
|
PasswordIsDefault bool `json:"password_is_default" example:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitialPasswordResponse is returned by the one-time initial password API.
|
||||||
|
type InitialPasswordResponse struct {
|
||||||
|
Username string `json:"username" example:"admin"`
|
||||||
|
Password string `json:"password" example:"a1b2c3d4e5f6"`
|
||||||
|
}
|
||||||
|
|
||||||
// Login verifies credentials, stamps last_login_at, returns a signed JWT.
|
// Login verifies credentials, stamps last_login_at, returns a signed JWT.
|
||||||
// @Summary Admin login
|
// @Summary Admin login
|
||||||
// @Description Verify credentials and return a signed JWT token
|
// @Description Verify credentials and return a signed JWT token
|
||||||
@@ -77,6 +83,42 @@ func (c *BaseAdminController) Login(ctx echo.Context) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInitialPassword returns the one-time initial admin password.
|
||||||
|
// @Summary Get initial admin password (one-time)
|
||||||
|
// @Description Returns the initial random admin password once, then invalidates it.
|
||||||
|
// @Tags Admin Auth
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.ApiResponse{data=admin.InitialPasswordResponse}
|
||||||
|
// @Failure 400 {object} response.ApiResponse
|
||||||
|
// @Router /admin/api/v1/auth/init-password [get]
|
||||||
|
func (c *BaseAdminController) GetInitialPassword(ctx echo.Context) error {
|
||||||
|
password, err := data.ConsumeInitialAdminPassword()
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
return c.SucJson(ctx, InitialPasswordResponse{
|
||||||
|
Username: "admin",
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInitialPasswordHash returns the initial-password fingerprint so the
|
||||||
|
// frontend can warn until the password is changed.
|
||||||
|
// @Summary Get initial admin password hash
|
||||||
|
// @Description Returns the initial password hash and password-changed flag.
|
||||||
|
// @Tags Admin Auth
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.ApiResponse{data=data.InitialAdminPasswordHashInfo}
|
||||||
|
// @Failure 400 {object} response.ApiResponse
|
||||||
|
// @Router /admin/api/v1/auth/init-password-hash [get]
|
||||||
|
func (c *BaseAdminController) GetInitialPasswordHash(ctx echo.Context) error {
|
||||||
|
info, err := data.GetInitialAdminPasswordHashInfo()
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
return c.SucJson(ctx, info)
|
||||||
|
}
|
||||||
|
|
||||||
// Logout is a no-op stub — tokens are stateless and the frontend
|
// Logout is a no-op stub — tokens are stateless and the frontend
|
||||||
// discards them. Kept for API symmetry with the documented spec.
|
// discards them. Kept for API symmetry with the documented spec.
|
||||||
// @Summary Admin logout
|
// @Summary Admin logout
|
||||||
@@ -111,9 +153,9 @@ func (c *BaseAdminController) Me(ctx echo.Context) error {
|
|||||||
if user.ID == 0 {
|
if user.ID == 0 {
|
||||||
return c.FailJson(ctx, errors.New("user not found"))
|
return c.FailJson(ctx, errors.New("user not found"))
|
||||||
}
|
}
|
||||||
// Warn the frontend when the operator hasn't changed the default
|
// Warn the frontend when the operator hasn't changed the seeded
|
||||||
// password so the UI can show a prominent reminder.
|
// initial password.
|
||||||
isDefault := data.VerifyPassword(user.PasswordHash, "admin")
|
isDefault := data.IsUsingInitialAdminPassword()
|
||||||
return c.SucJson(ctx, MeResponse{
|
return c.SucJson(ctx, MeResponse{
|
||||||
AdminUser: *user,
|
AdminUser: *user,
|
||||||
PasswordIsDefault: isDefault,
|
PasswordIsDefault: isDefault,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/assimon/luuu/controller"
|
"github.com/GMWalletApp/epusdt/controller"
|
||||||
"github.com/assimon/luuu/middleware"
|
"github.com/GMWalletApp/epusdt/middleware"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/notify"
|
"github.com/GMWalletApp/epusdt/notify"
|
||||||
"github.com/assimon/luuu/telegram"
|
"github.com/GMWalletApp/epusdt/telegram"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/GMWalletApp/epusdt/model/request"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/task"
|
"github.com/GMWalletApp/epusdt/task"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/telegram"
|
"github.com/GMWalletApp/epusdt/telegram"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/assimon/luuu/util/http"
|
"github.com/GMWalletApp/epusdt/util/http"
|
||||||
"github.com/gookit/validate"
|
"github.com/gookit/validate"
|
||||||
"github.com/gookit/validate/locales/zhcn"
|
"github.com/gookit/validate/locales/zhcn"
|
||||||
"github.com/gookit/validate/locales/zhtw"
|
"github.com/gookit/validate/locales/zhtw"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package comm
|
package comm
|
||||||
|
|
||||||
import "github.com/assimon/luuu/controller"
|
import "github.com/GMWalletApp/epusdt/controller"
|
||||||
|
|
||||||
var Ctrl = &BaseCommController{}
|
var Ctrl = &BaseCommController{}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/assimon/luuu/middleware"
|
"github.com/GMWalletApp/epusdt/middleware"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/GMWalletApp/epusdt/model/request"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/model/response"
|
"github.com/GMWalletApp/epusdt/model/response"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/response"
|
"github.com/GMWalletApp/epusdt/model/response"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
module github.com/assimon/luuu
|
module github.com/GMWalletApp/epusdt
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
luluHttp "github.com/GMWalletApp/epusdt/util/http"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
@@ -195,6 +196,9 @@ func RunInstallServer(listenAddr, envFilePath string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||||
|
Skipper: func(c echo.Context) bool {
|
||||||
|
return luluHttp.ShouldSkipSPAFallback(c.Request().URL.Path)
|
||||||
|
},
|
||||||
HTML5: true,
|
HTML5: true,
|
||||||
Index: "index.html",
|
Index: "index.html",
|
||||||
Root: wwwRoot,
|
Root: wwwRoot,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
appLog "github.com/assimon/luuu/util/log"
|
appLog "github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/libtnb/sqlite"
|
"github.com/libtnb/sqlite"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/assimon/luuu/command"
|
"github.com/GMWalletApp/epusdt/command"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
appjwt "github.com/assimon/luuu/util/jwt"
|
appjwt "github.com/GMWalletApp/epusdt/util/jwt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/assimon/luuu/util/json"
|
"github.com/GMWalletApp/epusdt/util/json"
|
||||||
"github.com/assimon/luuu/util/sign"
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package dao
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
|
|||||||
@@ -2,16 +2,38 @@ package data
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/dromara/carbon/v2"
|
"github.com/dromara/carbon/v2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultAdminUsername = "admin"
|
const defaultAdminUsername = "admin"
|
||||||
|
const InitialAdminPasswordHashAlgorithm = "sha256"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInitAdminPasswordUnavailable = errors.New("initial password unavailable")
|
||||||
|
ErrInitAdminPasswordAlreadyFetched = errors.New("initial password already fetched")
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitialAdminPasswordHashInfo is returned by the hash query endpoint
|
||||||
|
// so the frontend can detect whether the operator is still using the
|
||||||
|
// initial password.
|
||||||
|
type InitialAdminPasswordHashInfo struct {
|
||||||
|
Algorithm string `json:"algorithm" example:"sha256"`
|
||||||
|
PasswordHash string `json:"password_hash" example:"3f79bb7b435b05321651daefd374cdc9f5f72c467ea3f9f3c5f6e6d7e8f9a0b1"`
|
||||||
|
PasswordChanged bool `json:"password_changed" example:"false"`
|
||||||
|
Available bool `json:"available" example:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
// EnsureDefaultAdmin seeds an initial admin account when no admin user
|
// EnsureDefaultAdmin seeds an initial admin account when no admin user
|
||||||
// exists. The password is randomly generated and returned so the caller
|
// exists. The password is randomly generated and returned so the caller
|
||||||
@@ -35,7 +57,15 @@ func EnsureDefaultAdmin() (password string, created bool, err error) {
|
|||||||
PasswordHash: hash,
|
PasswordHash: hash,
|
||||||
Status: mdb.AdminUserStatusEnable,
|
Status: mdb.AdminUserStatusEnable,
|
||||||
}
|
}
|
||||||
return password, true, dao.Mdb.Create(user).Error
|
if err := dao.Mdb.Create(user).Error; err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
if err := initAdminPasswordState(password); err != nil {
|
||||||
|
// Account was already created; surface both for visibility while
|
||||||
|
// preserving created=true and password for emergency fallback.
|
||||||
|
return password, true, err
|
||||||
|
}
|
||||||
|
return password, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomAdminPassword() string {
|
func randomAdminPassword() string {
|
||||||
@@ -44,6 +74,141 @@ func randomAdminPassword() string {
|
|||||||
return hex.EncodeToString(b)
|
return hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HashInitialAdminPassword fingerprints the initial plaintext password so
|
||||||
|
// the frontend can compare user input locally without exposing plaintext.
|
||||||
|
func HashInitialAdminPassword(plain string) string {
|
||||||
|
sum := sha256.Sum256([]byte(plain))
|
||||||
|
return fmt.Sprintf("%x", sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func initAdminPasswordState(plain string) error {
|
||||||
|
hash := HashInitialAdminPassword(plain)
|
||||||
|
settings := []mdb.Setting{
|
||||||
|
{
|
||||||
|
Group: mdb.SettingGroupSystem, Key: mdb.SettingKeyInitAdminPasswordPlain,
|
||||||
|
Value: plain, Type: mdb.SettingTypeString,
|
||||||
|
Description: "One-time readable initial admin password",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Group: mdb.SettingGroupSystem, Key: mdb.SettingKeyInitAdminPasswordHash,
|
||||||
|
Value: hash, Type: mdb.SettingTypeString,
|
||||||
|
Description: "SHA-256 fingerprint for initial admin password",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Group: mdb.SettingGroupSystem, Key: mdb.SettingKeyInitAdminPasswordFetched,
|
||||||
|
Value: "false", Type: mdb.SettingTypeBool,
|
||||||
|
Description: "Whether initial admin password has been fetched",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Group: mdb.SettingGroupSystem, Key: mdb.SettingKeyInitAdminPasswordChanged,
|
||||||
|
Value: "false", Type: mdb.SettingTypeBool,
|
||||||
|
Description: "Whether initial admin password has been changed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, row := range settings {
|
||||||
|
if err := upsertSettingRow(dao.Mdb, row); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keep in-process cache coherent for the current process.
|
||||||
|
settingsCacheMu.Lock()
|
||||||
|
settingsCache[mdb.SettingKeyInitAdminPasswordPlain] = plain
|
||||||
|
settingsCache[mdb.SettingKeyInitAdminPasswordHash] = hash
|
||||||
|
settingsCache[mdb.SettingKeyInitAdminPasswordFetched] = "false"
|
||||||
|
settingsCache[mdb.SettingKeyInitAdminPasswordChanged] = "false"
|
||||||
|
settingsCacheMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertSettingRow(tx *gorm.DB, row mdb.Setting) error {
|
||||||
|
return tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "key"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"group", "value", "type", "description", "updated_at"}),
|
||||||
|
}).Create(&row).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeInitialAdminPassword returns the one-time initial admin password.
|
||||||
|
// After a successful read, the plaintext is deleted and cannot be fetched
|
||||||
|
// again.
|
||||||
|
func ConsumeInitialAdminPassword() (string, error) {
|
||||||
|
var password string
|
||||||
|
err := dao.Mdb.Transaction(func(tx *gorm.DB) error {
|
||||||
|
row := new(mdb.Setting)
|
||||||
|
if err := tx.Model(&mdb.Setting{}).
|
||||||
|
Where("`key` = ?", mdb.SettingKeyInitAdminPasswordPlain).
|
||||||
|
Limit(1).
|
||||||
|
Find(row).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if row.ID == 0 || row.Value == "" {
|
||||||
|
var fetched mdb.Setting
|
||||||
|
if err := tx.Model(&mdb.Setting{}).
|
||||||
|
Where("`key` = ?", mdb.SettingKeyInitAdminPasswordFetched).
|
||||||
|
Limit(1).
|
||||||
|
Find(&fetched).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(fetched.Value), "true") {
|
||||||
|
return ErrInitAdminPasswordAlreadyFetched
|
||||||
|
}
|
||||||
|
return ErrInitAdminPasswordUnavailable
|
||||||
|
}
|
||||||
|
password = row.Value
|
||||||
|
res := tx.Where("`key` = ?", mdb.SettingKeyInitAdminPasswordPlain).Delete(&mdb.Setting{})
|
||||||
|
if res.Error != nil {
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return ErrInitAdminPasswordAlreadyFetched
|
||||||
|
}
|
||||||
|
return upsertSettingRow(tx, mdb.Setting{
|
||||||
|
Group: mdb.SettingGroupSystem,
|
||||||
|
Key: mdb.SettingKeyInitAdminPasswordFetched,
|
||||||
|
Value: "true",
|
||||||
|
Type: mdb.SettingTypeBool,
|
||||||
|
Description: "Whether initial admin password has been fetched",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
settingsCacheMu.Lock()
|
||||||
|
delete(settingsCache, mdb.SettingKeyInitAdminPasswordPlain)
|
||||||
|
settingsCache[mdb.SettingKeyInitAdminPasswordFetched] = "true"
|
||||||
|
settingsCacheMu.Unlock()
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInitialAdminPasswordHashInfo returns the initial-password fingerprint
|
||||||
|
// and the changed-state flag used by the admin frontend.
|
||||||
|
func GetInitialAdminPasswordHashInfo() (*InitialAdminPasswordHashInfo, error) {
|
||||||
|
hash := strings.TrimSpace(GetSettingString(mdb.SettingKeyInitAdminPasswordHash, ""))
|
||||||
|
if hash == "" {
|
||||||
|
return &InitialAdminPasswordHashInfo{
|
||||||
|
Algorithm: InitialAdminPasswordHashAlgorithm,
|
||||||
|
PasswordHash: "",
|
||||||
|
PasswordChanged: true,
|
||||||
|
Available: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return &InitialAdminPasswordHashInfo{
|
||||||
|
Algorithm: InitialAdminPasswordHashAlgorithm,
|
||||||
|
PasswordHash: hash,
|
||||||
|
PasswordChanged: GetSettingBool(mdb.SettingKeyInitAdminPasswordChanged, false),
|
||||||
|
Available: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUsingInitialAdminPassword reports whether the current admin password is
|
||||||
|
// still considered the seeded initial password.
|
||||||
|
func IsUsingInitialAdminPassword() bool {
|
||||||
|
hash := strings.TrimSpace(GetSettingString(mdb.SettingKeyInitAdminPasswordHash, ""))
|
||||||
|
if hash == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !GetSettingBool(mdb.SettingKeyInitAdminPasswordChanged, false)
|
||||||
|
}
|
||||||
|
|
||||||
// HashPassword bcrypts a plaintext password.
|
// HashPassword bcrypts a plaintext password.
|
||||||
func HashPassword(plain string) (string, error) {
|
func HashPassword(plain string) (string, error) {
|
||||||
b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
|
b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
|
||||||
@@ -80,9 +245,55 @@ func UpdateAdminUserPassword(id uint64, newPlain string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return dao.Mdb.Model(&mdb.AdminUser{}).
|
changedCacheValue := ""
|
||||||
Where("id = ?", id).
|
err = dao.Mdb.Transaction(func(tx *gorm.DB) error {
|
||||||
Update("password_hash", hash).Error
|
if err := tx.Model(&mdb.AdminUser{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Update("password_hash", hash).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old installs may not have initial-password metadata; skip in
|
||||||
|
// that case to keep password updates backward compatible.
|
||||||
|
var initHashRow mdb.Setting
|
||||||
|
if err := tx.Model(&mdb.Setting{}).
|
||||||
|
Where("`key` = ?", mdb.SettingKeyInitAdminPasswordHash).
|
||||||
|
Limit(1).
|
||||||
|
Find(&initHashRow).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
initHash := strings.TrimSpace(initHashRow.Value)
|
||||||
|
if initHash == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newHash := HashInitialAdminPassword(newPlain)
|
||||||
|
changed := subtle.ConstantTimeCompare([]byte(initHash), []byte(newHash)) != 1
|
||||||
|
changedValue := "true"
|
||||||
|
if !changed {
|
||||||
|
changedValue = "false"
|
||||||
|
}
|
||||||
|
if err := upsertSettingRow(tx, mdb.Setting{
|
||||||
|
Group: mdb.SettingGroupSystem,
|
||||||
|
Key: mdb.SettingKeyInitAdminPasswordChanged,
|
||||||
|
Value: changedValue,
|
||||||
|
Type: mdb.SettingTypeBool,
|
||||||
|
Description: "Whether initial admin password has been changed",
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
changedCacheValue = changedValue
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if changedCacheValue != "" {
|
||||||
|
settingsCacheMu.Lock()
|
||||||
|
settingsCache[mdb.SettingKeyInitAdminPasswordChanged] = changedCacheValue
|
||||||
|
settingsCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TouchAdminUserLastLogin stamps last_login_at to now.
|
// TouchAdminUserLastLogin stamps last_login_at to now.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/dromara/carbon/v2"
|
"github.com/dromara/carbon/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package data
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListChains returns all chain rows ordered by id.
|
// ListChains returns all chain rows ordered by id.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package data
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListEnabledChainTokens returns enabled tokens, optionally filtered by network.
|
// ListEnabledChainTokens returns enabled tokens, optionally filtered by network.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListNotificationChannels returns all rows, optionally filtered by type.
|
// ListNotificationChannels returns all rows, optionally filtered by type.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/GMWalletApp/epusdt/model/request"
|
||||||
"github.com/dromara/carbon/v2"
|
"github.com/dromara/carbon/v2"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package data
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DailyStat is one bucket of day-level aggregation used by the
|
// DailyStat is one bucket of day-level aggregation used by the
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/internal/testutil"
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEvmTransactionLockAddressIsCaseInsensitive(t *testing.T) {
|
func TestEvmTransactionLockAddressIsCaseInsensitive(t *testing.T) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/dromara/carbon/v2"
|
"github.com/dromara/carbon/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ func DeleteRpcNodeByID(id uint64) error {
|
|||||||
|
|
||||||
// SelectRpcNode picks a healthy RPC endpoint for a (network, type) pair.
|
// SelectRpcNode picks a healthy RPC endpoint for a (network, type) pair.
|
||||||
// Strategy: weighted random among rows where enabled=true AND status=ok.
|
// Strategy: weighted random among rows where enabled=true AND status=ok.
|
||||||
// Falls back to any enabled row when none are healthy yet (bootstrap case
|
// Falls back to enabled rows with status=unknown when no health check has
|
||||||
// where the health check hasn't run). Returns nil if nothing is defined.
|
// run yet. Explicitly down rows are not selected.
|
||||||
func SelectRpcNode(network, nodeType string) (*mdb.RpcNode, error) {
|
func SelectRpcNode(network, nodeType string) (*mdb.RpcNode, error) {
|
||||||
var rows []mdb.RpcNode
|
var rows []mdb.RpcNode
|
||||||
err := dao.Mdb.Model(&mdb.RpcNode{}).
|
err := dao.Mdb.Model(&mdb.RpcNode{}).
|
||||||
@@ -62,15 +62,22 @@ func SelectRpcNode(network, nodeType string) (*mdb.RpcNode, error) {
|
|||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
healthy := rows[:0]
|
healthy := make([]mdb.RpcNode, 0, len(rows))
|
||||||
|
bootstrap := make([]mdb.RpcNode, 0, len(rows))
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
if r.Status == mdb.RpcNodeStatusOk {
|
switch r.Status {
|
||||||
|
case mdb.RpcNodeStatusOk:
|
||||||
healthy = append(healthy, r)
|
healthy = append(healthy, r)
|
||||||
|
case "", mdb.RpcNodeStatusUnknown:
|
||||||
|
bootstrap = append(bootstrap, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
candidates := healthy
|
candidates := healthy
|
||||||
if len(candidates) == 0 {
|
if len(candidates) == 0 {
|
||||||
candidates = rows // bootstrap: no health check has run yet
|
candidates = bootstrap
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
return pickWeighted(candidates), nil
|
return pickWeighted(candidates), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSelectRpcNodeUsesHealthyRow(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||||
|
Network: mdb.NetworkSolana,
|
||||||
|
Url: "https://unknown.example.com",
|
||||||
|
Type: mdb.RpcNodeTypeHttp,
|
||||||
|
Weight: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Status: mdb.RpcNodeStatusUnknown,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed unknown rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||||
|
Network: mdb.NetworkSolana,
|
||||||
|
Url: "https://ok.example.com",
|
||||||
|
Type: mdb.RpcNodeTypeHttp,
|
||||||
|
Weight: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Status: mdb.RpcNodeStatusOk,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed ok rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := SelectRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectRpcNode(): %v", err)
|
||||||
|
}
|
||||||
|
if got == nil || got.Url != "https://ok.example.com" {
|
||||||
|
t.Fatalf("SelectRpcNode() = %#v, want ok row", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectRpcNodeFallsBackToUnknownOnly(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||||
|
Network: mdb.NetworkSolana,
|
||||||
|
Url: "https://unknown.example.com",
|
||||||
|
Type: mdb.RpcNodeTypeHttp,
|
||||||
|
Weight: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Status: mdb.RpcNodeStatusUnknown,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed unknown rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||||
|
Network: mdb.NetworkSolana,
|
||||||
|
Url: "https://down.example.com",
|
||||||
|
Type: mdb.RpcNodeTypeHttp,
|
||||||
|
Weight: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Status: mdb.RpcNodeStatusDown,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed down rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := SelectRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectRpcNode(): %v", err)
|
||||||
|
}
|
||||||
|
if got == nil || got.Url != "https://unknown.example.com" {
|
||||||
|
t.Fatalf("SelectRpcNode() = %#v, want unknown row", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectRpcNodeIgnoresDownRows(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||||
|
Network: mdb.NetworkSolana,
|
||||||
|
Url: "https://down.example.com",
|
||||||
|
Type: mdb.RpcNodeTypeHttp,
|
||||||
|
Weight: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Status: mdb.RpcNodeStatusDown,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed down rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := SelectRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectRpcNode(): %v", err)
|
||||||
|
}
|
||||||
|
if got != nil {
|
||||||
|
t.Fatalf("SelectRpcNode() = %#v, want nil", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,6 +151,10 @@ func DeleteSetting(key string) error {
|
|||||||
// sensitiveSettingKeys lists keys that must never be returned to API callers.
|
// sensitiveSettingKeys lists keys that must never be returned to API callers.
|
||||||
var sensitiveSettingKeys = []string{
|
var sensitiveSettingKeys = []string{
|
||||||
mdb.SettingKeyJwtSecret,
|
mdb.SettingKeyJwtSecret,
|
||||||
|
mdb.SettingKeyInitAdminPasswordPlain,
|
||||||
|
mdb.SettingKeyInitAdminPasswordHash,
|
||||||
|
mdb.SettingKeyInitAdminPasswordFetched,
|
||||||
|
mdb.SettingKeyInitAdminPasswordChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSettingsByGroup returns all rows for a given group (empty group = all),
|
// ListSettingsByGroup returns all rows for a given group (empty group = all),
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package data
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddWalletAddress 创建钱包 (默认 tron 网络,用于 Telegram 添加)
|
// AddWalletAddress 创建钱包 (默认 tron 网络,用于 Telegram 添加)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/assimon/luuu/internal/testutil"
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAddWalletAddressWithNetworkNormalizesEvmAddressToLowercase(t *testing.T) {
|
func TestAddWalletAddressWithNetworkNormalizesEvmAddressToLowercase(t *testing.T) {
|
||||||
|
|||||||
+15
-11
@@ -22,17 +22,21 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SettingKeyJwtSecret = "system.jwt_secret"
|
SettingKeyJwtSecret = "system.jwt_secret"
|
||||||
SettingKeyOrderExpiration = "system.order_expiration_time"
|
SettingKeyInitAdminPasswordPlain = "system.init_admin_password_plain"
|
||||||
SettingKeyBrandSiteName = "brand.site_name"
|
SettingKeyInitAdminPasswordHash = "system.init_admin_password_hash"
|
||||||
SettingKeyBrandLogoUrl = "brand.logo_url"
|
SettingKeyInitAdminPasswordFetched = "system.init_admin_password_fetched"
|
||||||
SettingKeyBrandPageTitle = "brand.page_title"
|
SettingKeyInitAdminPasswordChanged = "system.init_admin_password_changed"
|
||||||
SettingKeyBrandPaySuccess = "brand.pay_success_text"
|
SettingKeyOrderExpiration = "system.order_expiration_time"
|
||||||
SettingKeyBrandSupportUrl = "brand.support_url"
|
SettingKeyBrandSiteName = "brand.site_name"
|
||||||
SettingKeyRateForcedUsdt = "rate.forced_usdt_rate"
|
SettingKeyBrandLogoUrl = "brand.logo_url"
|
||||||
SettingKeyRateAdjustPercent = "rate.adjust_percent"
|
SettingKeyBrandPageTitle = "brand.page_title"
|
||||||
SettingKeyRateOkxC2cEnabled = "rate.okx_c2c_enabled"
|
SettingKeyBrandPaySuccess = "brand.pay_success_text"
|
||||||
SettingKeyRateApiUrl = "rate.api_url"
|
SettingKeyBrandSupportUrl = "brand.support_url"
|
||||||
|
SettingKeyRateForcedUsdt = "rate.forced_usdt_rate"
|
||||||
|
SettingKeyRateAdjustPercent = "rate.adjust_percent"
|
||||||
|
SettingKeyRateOkxC2cEnabled = "rate.okx_c2c_enabled"
|
||||||
|
SettingKeyRateApiUrl = "rate.api_url"
|
||||||
|
|
||||||
// EPAY route defaults — can be overridden via admin settings.
|
// EPAY route defaults — can be overridden via admin settings.
|
||||||
SettingKeyEpayDefaultToken = "epay.default_token"
|
SettingKeyEpayDefaultToken = "epay.default_token"
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/GMWalletApp/epusdt/model/request"
|
||||||
"github.com/assimon/luuu/model/response"
|
"github.com/GMWalletApp/epusdt/model/response"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/assimon/luuu/util/math"
|
"github.com/GMWalletApp/epusdt/util/math"
|
||||||
"github.com/dromara/carbon/v2"
|
"github.com/dromara/carbon/v2"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/assimon/luuu/internal/testutil"
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/GMWalletApp/epusdt/model/request"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCreateTransactionRequest(orderID string, amount float64) *request.CreateTransactionRequest {
|
func newCreateTransactionRequest(orderID string, amount float64) *request.CreateTransactionRequest {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/response"
|
"github.com/GMWalletApp/epusdt/model/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrOrder = errors.New("不存在待支付订单或已过期")
|
var ErrOrder = errors.New("不存在待支付订单或已过期")
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/GMWalletApp/epusdt/model/request"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/assimon/luuu/util/math"
|
"github.com/GMWalletApp/epusdt/util/math"
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
@@ -281,6 +281,21 @@ type solSignatureResult struct {
|
|||||||
BlockTime *int64 `json:"blockTime"`
|
BlockTime *int64 `json:"blockTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveSolanaRpcURL() (string, error) {
|
||||||
|
node, err := data.SelectRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if node == nil || node.ID == 0 {
|
||||||
|
return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
|
||||||
|
}
|
||||||
|
rpcURL := strings.TrimSpace(node.Url)
|
||||||
|
if rpcURL == "" {
|
||||||
|
return "", fmt.Errorf("rpc_nodes id=%d has empty url", node.ID)
|
||||||
|
}
|
||||||
|
return rpcURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SolRetryClient 发送 Solana JSON-RPC 请求,自动重试
|
// SolRetryClient 发送 Solana JSON-RPC 请求,自动重试
|
||||||
func SolRetryClient(method string, params []interface{}) ([]byte, error) {
|
func SolRetryClient(method string, params []interface{}) ([]byte, error) {
|
||||||
client := resty.New()
|
client := resty.New()
|
||||||
@@ -297,7 +312,10 @@ func SolRetryClient(method string, params []interface{}) ([]byte, error) {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
rpcUrl := config.GetSolanaRpcUrl()
|
rpcUrl, err := resolveSolanaRpcURL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.R().
|
resp, err := client.R().
|
||||||
SetHeader("Content-Type", "application/json").
|
SetHeader("Content-Type", "application/json").
|
||||||
|
|||||||
@@ -2,13 +2,59 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func setupSolanaRPCNode(t *testing.T, url string) func() {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
node := &mdb.RpcNode{
|
||||||
|
Network: mdb.NetworkSolana,
|
||||||
|
Url: url,
|
||||||
|
Type: mdb.RpcNodeTypeHttp,
|
||||||
|
Weight: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Status: mdb.RpcNodeStatusOk,
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Create(node).Error; err != nil {
|
||||||
|
cleanup()
|
||||||
|
t.Fatalf("seed solana rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
return cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSolanaRpcURLRequiresRpcNode(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if got, err := resolveSolanaRpcURL(); err == nil {
|
||||||
|
t.Fatalf("resolveSolanaRpcURL() = %q, nil; want error", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSolanaRpcURLWithRow(t *testing.T) {
|
||||||
|
cleanup := setupSolanaRPCNode(t, " https://solana.example.com ")
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
got, err := resolveSolanaRpcURL()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveSolanaRpcURL(): %v", err)
|
||||||
|
}
|
||||||
|
if got != "https://solana.example.com" {
|
||||||
|
t.Fatalf("resolveSolanaRpcURL() = %q, want https://solana.example.com", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSolClientHealthy(t *testing.T) {
|
func TestSolClientHealthy(t *testing.T) {
|
||||||
|
requireSolanaIntegration(t)
|
||||||
|
|
||||||
bodyData, err := SolRetryClient("getHealth", nil)
|
bodyData, err := SolRetryClient("getHealth", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SolRetryClient failed: %v", err)
|
t.Fatalf("SolRetryClient failed: %v", err)
|
||||||
@@ -33,6 +79,8 @@ func TestSolClientHealthy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSolClientGetSignaturesForAddress(t *testing.T) {
|
func TestSolClientGetSignaturesForAddress(t *testing.T) {
|
||||||
|
requireSolanaIntegration(t)
|
||||||
|
|
||||||
// Example wallet address (replace with actual test address)
|
// Example wallet address (replace with actual test address)
|
||||||
address := "2uFTf9TZ8gd7Kg6hkb79TxfaeNpaAgpJ8uVHguv2Yweu"
|
address := "2uFTf9TZ8gd7Kg6hkb79TxfaeNpaAgpJ8uVHguv2Yweu"
|
||||||
|
|
||||||
@@ -57,6 +105,8 @@ func TestSolClientGetSignaturesForAddress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSolClientGetTransaction(t *testing.T) {
|
func TestSolClientGetTransaction(t *testing.T) {
|
||||||
|
requireSolanaIntegration(t)
|
||||||
|
|
||||||
// Example transaction signature (replace with actual test signature)
|
// Example transaction signature (replace with actual test signature)
|
||||||
sig := "2aEoNykk4ZJ27C3y7EDJiQUc7GFnnsMe7ofFzB73swGL8kTxSBFCnwzWw3jzr3BND7k8hx15fZHUUAbG1XemNFe5"
|
sig := "2aEoNykk4ZJ27C3y7EDJiQUc7GFnnsMe7ofFzB73swGL8kTxSBFCnwzWw3jzr3BND7k8hx15fZHUUAbG1XemNFe5"
|
||||||
|
|
||||||
@@ -64,7 +114,6 @@ func TestSolClientGetTransaction(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SolRetryClient failed: %v", err)
|
t.Fatalf("SolRetryClient failed: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%v\n", string(txData))
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
err = json.Unmarshal(txData, &result)
|
err = json.Unmarshal(txData, &result)
|
||||||
@@ -80,6 +129,15 @@ func TestSolClientGetTransaction(t *testing.T) {
|
|||||||
t.Logf("Transaction Info for signature %s: %v", sig, txInfo)
|
t.Logf("Transaction Info for signature %s: %v", sig, txInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireSolanaIntegration(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if testing.Short() || os.Getenv("RUN_SOLANA_INTEGRATION_TESTS") == "" {
|
||||||
|
t.Skip("set RUN_SOLANA_INTEGRATION_TESTS=1 to run Solana public RPC integration tests")
|
||||||
|
}
|
||||||
|
cleanup := setupSolanaRPCNode(t, "https://api.mainnet-beta.solana.com")
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFindATAAddress(t *testing.T) {
|
func TestFindATAAddress(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -219,9 +277,7 @@ func TestAdjustAmount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTransferInfoFromInstruction_SplTransfer(t *testing.T) {
|
func TestParseTransferInfoFromInstruction_SplTransfer(t *testing.T) {
|
||||||
if testing.Short() {
|
requireSolanaIntegration(t)
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SPL Token "transfer" (no mint in instruction, must look up from postTokenBalances)
|
// SPL Token "transfer" (no mint in instruction, must look up from postTokenBalances)
|
||||||
sig := "3tZTwLrvmiZ59h4UzyMHPd7DPux7t9eXZgkUvEfquaoSuERrPSRNzWuSHKQM2fbiCWFDGNqoLpu2kLZnfoegVpqN"
|
sig := "3tZTwLrvmiZ59h4UzyMHPd7DPux7t9eXZgkUvEfquaoSuERrPSRNzWuSHKQM2fbiCWFDGNqoLpu2kLZnfoegVpqN"
|
||||||
@@ -261,9 +317,7 @@ func TestParseTransferInfoFromInstruction_SplTransfer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTransferInfoFromInstruction_TransferChecked(t *testing.T) {
|
func TestParseTransferInfoFromInstruction_TransferChecked(t *testing.T) {
|
||||||
if testing.Short() {
|
requireSolanaIntegration(t)
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SPL Token "transferChecked" (has mint and tokenAmount in instruction)
|
// SPL Token "transferChecked" (has mint and tokenAmount in instruction)
|
||||||
sig := "2aEoNykk4ZJ27C3y7EDJiQUc7GFnnsMe7ofFzB73swGL8kTxSBFCnwzWw3jzr3BND7k8hx15fZHUUAbG1XemNFe5"
|
sig := "2aEoNykk4ZJ27C3y7EDJiQUc7GFnnsMe7ofFzB73swGL8kTxSBFCnwzWw3jzr3BND7k8hx15fZHUUAbG1XemNFe5"
|
||||||
@@ -306,9 +360,7 @@ func TestParseTransferInfoFromInstruction_TransferChecked(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTransferInfoFromInstruction_SystemTransfer(t *testing.T) {
|
func TestParseTransferInfoFromInstruction_SystemTransfer(t *testing.T) {
|
||||||
if testing.Short() {
|
requireSolanaIntegration(t)
|
||||||
t.Skip("Skipping integration test")
|
|
||||||
}
|
|
||||||
|
|
||||||
// System program SOL transfer
|
// System program SOL transfer
|
||||||
sig := "5pNMonUBvLVpxXTmyd5CGVBs49W6781g2ACnrCXhbmtz58KENYA7HSqu6hQkQweg3qQboRd8WAscphNAtiq9UtZZ"
|
sig := "5pNMonUBvLVpxXTmyd5CGVBs49W6781g2ACnrCXhbmtz58KENYA7HSqu6hQkQweg3qQboRd8WAscphNAtiq9UtZZ"
|
||||||
|
|||||||
@@ -9,16 +9,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
tron "github.com/GMWalletApp/epusdt/crypto"
|
||||||
tron "github.com/assimon/luuu/crypto"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/request"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/GMWalletApp/epusdt/notify"
|
||||||
"github.com/assimon/luuu/notify"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/http_client"
|
||||||
"github.com/assimon/luuu/util/http_client"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/math"
|
||||||
"github.com/assimon/luuu/util/math"
|
|
||||||
"github.com/dromara/carbon/v2"
|
"github.com/dromara/carbon/v2"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/gookit/goutil/stdutil"
|
"github.com/gookit/goutil/stdutil"
|
||||||
@@ -26,19 +25,21 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tronNodeDefault is the fallback when no rpc_nodes row exists for TRON.
|
|
||||||
const tronNodeDefault = "https://api.trongrid.io"
|
|
||||||
|
|
||||||
// resolveTronNode returns (baseURL, apiKey) for the TRON HTTP RPC node.
|
// resolveTronNode returns (baseURL, apiKey) for the TRON HTTP RPC node.
|
||||||
// It reads the first healthy (or any enabled) row from the rpc_nodes table;
|
// It reads the first healthy (or any enabled) row from the rpc_nodes table.
|
||||||
// if none exists it falls back to the hard-coded default URL and the
|
func resolveTronNode() (string, string, error) {
|
||||||
// tron_grid_api_key value from .env (config.TRON_GRID_API_KEY).
|
|
||||||
func resolveTronNode() (string, string) {
|
|
||||||
node, err := data.SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
node, err := data.SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||||
if err == nil && node != nil && node.ID > 0 {
|
if err != nil {
|
||||||
return strings.TrimRight(node.Url, "/"), node.ApiKey
|
return "", "", err
|
||||||
}
|
}
|
||||||
return tronNodeDefault, config.TRON_GRID_API_KEY
|
if node == nil || node.ID == 0 {
|
||||||
|
return "", "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkTron, mdb.RpcNodeTypeHttp)
|
||||||
|
}
|
||||||
|
rpcURL := strings.TrimRight(strings.TrimSpace(node.Url), "/")
|
||||||
|
if rpcURL == "" {
|
||||||
|
return "", "", fmt.Errorf("rpc_nodes id=%d has empty url", node.ID)
|
||||||
|
}
|
||||||
|
return rpcURL, node.ApiKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Trc20CallBack(address string, wg *sync.WaitGroup) {
|
func Trc20CallBack(address string, wg *sync.WaitGroup) {
|
||||||
@@ -79,7 +80,11 @@ func checkTrxTransfers(address string, wg *sync.WaitGroup) {
|
|||||||
client := http_client.GetHttpClient()
|
client := http_client.GetHttpClient()
|
||||||
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
||||||
endTime := carbon.Now().TimestampMilli()
|
endTime := carbon.Now().TimestampMilli()
|
||||||
tronBaseURL, tronAPIKey := resolveTronNode()
|
tronBaseURL, tronAPIKey, err := resolveTronNode()
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[TRX][%s] resolve rpc_nodes err=%v", address, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
url := fmt.Sprintf("%s/v1/accounts/%s/transactions", tronBaseURL, address)
|
url := fmt.Sprintf("%s/v1/accounts/%s/transactions", tronBaseURL, address)
|
||||||
|
|
||||||
resp, err := client.R().SetQueryParams(map[string]string{
|
resp, err := client.R().SetQueryParams(map[string]string{
|
||||||
@@ -217,7 +222,11 @@ func checkTrc20Transfers(address string, wg *sync.WaitGroup) {
|
|||||||
client := http_client.GetHttpClient()
|
client := http_client.GetHttpClient()
|
||||||
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
||||||
endTime := carbon.Now().TimestampMilli()
|
endTime := carbon.Now().TimestampMilli()
|
||||||
tronBaseURL, tronAPIKey := resolveTronNode()
|
tronBaseURL, tronAPIKey, err := resolveTronNode()
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[TRC20][%s] resolve rpc_nodes err=%v", address, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
url := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20", tronBaseURL, address)
|
url := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20", tronBaseURL, address)
|
||||||
|
|
||||||
resp, err := client.R().SetQueryParams(map[string]string{
|
resp, err := client.R().SetQueryParams(map[string]string{
|
||||||
|
|||||||
@@ -3,26 +3,19 @@ package service
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
"github.com/assimon/luuu/internal/testutil"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestResolveTronNode_NoRow verifies that resolveTronNode falls back to the
|
// TestResolveTronNode_NoRow verifies that resolveTronNode requires an enabled
|
||||||
// hard-coded default when the rpc_nodes table has no TRON row.
|
// TRON row from rpc_nodes.
|
||||||
func TestResolveTronNode_NoRow(t *testing.T) {
|
func TestResolveTronNode_NoRow(t *testing.T) {
|
||||||
cleanup := testutil.SetupTestDatabases(t)
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
config.TRON_GRID_API_KEY = "fallback-key"
|
if gotURL, gotKey, err := resolveTronNode(); err == nil {
|
||||||
|
t.Fatalf("resolveTronNode() = (%q, %q, nil), want error", gotURL, gotKey)
|
||||||
gotURL, gotKey := resolveTronNode()
|
|
||||||
if gotURL != tronNodeDefault {
|
|
||||||
t.Errorf("url = %q, want %q", gotURL, tronNodeDefault)
|
|
||||||
}
|
|
||||||
if gotKey != "fallback-key" {
|
|
||||||
t.Errorf("apiKey = %q, want \"fallback-key\"", gotKey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +25,6 @@ func TestResolveTronNode_WithRow(t *testing.T) {
|
|||||||
cleanup := testutil.SetupTestDatabases(t)
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
config.TRON_GRID_API_KEY = "fallback-key"
|
|
||||||
|
|
||||||
// Insert an enabled TRON http node.
|
// Insert an enabled TRON http node.
|
||||||
node := &mdb.RpcNode{
|
node := &mdb.RpcNode{
|
||||||
Network: mdb.NetworkTron,
|
Network: mdb.NetworkTron,
|
||||||
@@ -48,7 +39,10 @@ func TestResolveTronNode_WithRow(t *testing.T) {
|
|||||||
t.Fatalf("seed rpc_node: %v", err)
|
t.Fatalf("seed rpc_node: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotURL, gotKey := resolveTronNode()
|
gotURL, gotKey, err := resolveTronNode()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveTronNode(): %v", err)
|
||||||
|
}
|
||||||
if gotURL != "https://custom-tron.example.com" {
|
if gotURL != "https://custom-tron.example.com" {
|
||||||
t.Errorf("url = %q, want https://custom-tron.example.com", gotURL)
|
t.Errorf("url = %q, want https://custom-tron.example.com", gotURL)
|
||||||
}
|
}
|
||||||
@@ -57,14 +51,11 @@ func TestResolveTronNode_WithRow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestResolveTronNode_DisabledRow verifies that a disabled row is ignored and
|
// TestResolveTronNode_DisabledRow verifies that a disabled row is ignored.
|
||||||
// the fallback is used.
|
|
||||||
func TestResolveTronNode_DisabledRow(t *testing.T) {
|
func TestResolveTronNode_DisabledRow(t *testing.T) {
|
||||||
cleanup := testutil.SetupTestDatabases(t)
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
config.TRON_GRID_API_KEY = "fallback-key"
|
|
||||||
|
|
||||||
node := &mdb.RpcNode{
|
node := &mdb.RpcNode{
|
||||||
Network: mdb.NetworkTron,
|
Network: mdb.NetworkTron,
|
||||||
Url: "https://disabled-tron.example.com",
|
Url: "https://disabled-tron.example.com",
|
||||||
@@ -82,11 +73,7 @@ func TestResolveTronNode_DisabledRow(t *testing.T) {
|
|||||||
t.Fatalf("disable rpc_node: %v", err)
|
t.Fatalf("disable rpc_node: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotURL, gotKey := resolveTronNode()
|
if gotURL, gotKey, err := resolveTronNode(); err == nil {
|
||||||
if gotURL != tronNodeDefault {
|
t.Fatalf("resolveTronNode() = (%q, %q, nil), want error", gotURL, gotKey)
|
||||||
t.Errorf("url = %q, want %q (disabled row should be ignored)", gotURL, tronNodeDefault)
|
|
||||||
}
|
|
||||||
if gotKey != "fallback-key" {
|
|
||||||
t.Errorf("apiKey = %q, want \"fallback-key\"", gotKey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ package mq
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
+8
-8
@@ -8,14 +8,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/response"
|
"github.com/GMWalletApp/epusdt/model/response"
|
||||||
"github.com/assimon/luuu/util/http_client"
|
"github.com/GMWalletApp/epusdt/util/http_client"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/assimon/luuu/util/sign"
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resolveOrderApiKey returns the api_keys row that signed the order.
|
// resolveOrderApiKey returns the api_keys row that signed the order.
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/internal/testutil"
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/util/sign"
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T) {
|
func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T) {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sender delivers an already-rendered text message via a channel row.
|
// Sender delivers an already-rendered text message via a channel row.
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,6 +118,14 @@ func doPutAdmin(e *echo.Echo, path string, body map[string]interface{}, token st
|
|||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doGet sends an unauthenticated GET request.
|
||||||
|
func doGet(e *echo.Echo, path string) *httptest.ResponseRecorder {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
// assertOK asserts the response status is 200 and the status_code field is 200.
|
// assertOK asserts the response status is 200 and the status_code field is 200.
|
||||||
func assertOK(t *testing.T, rec *httptest.ResponseRecorder) map[string]interface{} {
|
func assertOK(t *testing.T, rec *httptest.ResponseRecorder) map[string]interface{} {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -273,6 +281,84 @@ func TestAdminChangePassword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAdminInitPasswordFlow verifies the one-time initial password endpoint
|
||||||
|
// and hash-based "password changed" detection flow.
|
||||||
|
func TestAdminInitPasswordFlow(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
const initPassword = "init-pass-123456"
|
||||||
|
|
||||||
|
adminHash, err := data.HashPassword(initPassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HashPassword: %v", err)
|
||||||
|
}
|
||||||
|
dao.Mdb.Create(&mdb.AdminUser{
|
||||||
|
Username: testAdminUsername,
|
||||||
|
PasswordHash: adminHash,
|
||||||
|
Status: mdb.AdminUserStatusEnable,
|
||||||
|
})
|
||||||
|
_ = data.SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyInitAdminPasswordPlain, initPassword, mdb.SettingTypeString)
|
||||||
|
_ = data.SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyInitAdminPasswordHash, data.HashInitialAdminPassword(initPassword), mdb.SettingTypeString)
|
||||||
|
_ = data.SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyInitAdminPasswordFetched, "false", mdb.SettingTypeBool)
|
||||||
|
_ = data.SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyInitAdminPasswordChanged, "false", mdb.SettingTypeBool)
|
||||||
|
|
||||||
|
recHash := doGet(e, "/admin/api/v1/auth/init-password-hash")
|
||||||
|
respHash := assertOK(t, recHash)
|
||||||
|
hashData, _ := respHash["data"].(map[string]interface{})
|
||||||
|
if got := hashData["password_hash"]; got != data.HashInitialAdminPassword(initPassword) {
|
||||||
|
t.Fatalf("expected init hash %s, got %v", data.HashInitialAdminPassword(initPassword), got)
|
||||||
|
}
|
||||||
|
if got, _ := hashData["password_changed"].(bool); got {
|
||||||
|
t.Fatalf("expected password_changed=false before change, got true")
|
||||||
|
}
|
||||||
|
|
||||||
|
recFetch := doGet(e, "/admin/api/v1/auth/init-password")
|
||||||
|
respFetch := assertOK(t, recFetch)
|
||||||
|
fetchData, _ := respFetch["data"].(map[string]interface{})
|
||||||
|
if fetchData["username"] != testAdminUsername {
|
||||||
|
t.Fatalf("expected username=%s, got %v", testAdminUsername, fetchData["username"])
|
||||||
|
}
|
||||||
|
if fetchData["password"] != initPassword {
|
||||||
|
t.Fatalf("expected initial password %s, got %v", initPassword, fetchData["password"])
|
||||||
|
}
|
||||||
|
|
||||||
|
recFetch2 := doGet(e, "/admin/api/v1/auth/init-password")
|
||||||
|
if recFetch2.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("second fetch should fail with 400, got %d body=%s", recFetch2.Code, recFetch2.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(recFetch2.Body.String()), "already fetched") {
|
||||||
|
t.Fatalf("expected already fetched error, got: %s", recFetch2.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
token := adminLogin(t, e, testAdminUsername, initPassword)
|
||||||
|
|
||||||
|
recMe1 := doGetAdmin(e, "/admin/api/v1/auth/me", token)
|
||||||
|
respMe1 := assertOK(t, recMe1)
|
||||||
|
meData1, _ := respMe1["data"].(map[string]interface{})
|
||||||
|
if got, _ := meData1["password_is_default"].(bool); !got {
|
||||||
|
t.Fatalf("expected password_is_default=true before change, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
recChange := doPostAdmin(e, "/admin/api/v1/auth/password", map[string]interface{}{
|
||||||
|
"old_password": initPassword,
|
||||||
|
"new_password": "new-pass-789",
|
||||||
|
}, token)
|
||||||
|
assertOK(t, recChange)
|
||||||
|
|
||||||
|
recHash2 := doGet(e, "/admin/api/v1/auth/init-password-hash")
|
||||||
|
respHash2 := assertOK(t, recHash2)
|
||||||
|
hashData2, _ := respHash2["data"].(map[string]interface{})
|
||||||
|
if got, _ := hashData2["password_changed"].(bool); !got {
|
||||||
|
t.Fatalf("expected password_changed=true after change, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
recMe2 := doGetAdmin(e, "/admin/api/v1/auth/me", token)
|
||||||
|
respMe2 := assertOK(t, recMe2)
|
||||||
|
meData2, _ := respMe2["data"].(map[string]interface{})
|
||||||
|
if got, _ := meData2["password_is_default"].(bool); got {
|
||||||
|
t.Fatalf("expected password_is_default=false after change, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── API Keys ────────────────────────────────────────────────────────────────
|
// ─── API Keys ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// TestAdminApiKeys_CRUD verifies list, create, update, status change, secret,
|
// TestAdminApiKeys_CRUD verifies list, create, update, status change, secret,
|
||||||
|
|||||||
+9
-7
@@ -9,13 +9,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/assimon/luuu/controller/admin"
|
"github.com/GMWalletApp/epusdt/controller/admin"
|
||||||
"github.com/assimon/luuu/controller/comm"
|
"github.com/GMWalletApp/epusdt/controller/comm"
|
||||||
"github.com/assimon/luuu/middleware"
|
"github.com/GMWalletApp/epusdt/middleware"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/assimon/luuu/util/sign"
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
||||||
)
|
)
|
||||||
@@ -188,6 +188,8 @@ func registerAdminRoutes(e *echo.Echo) {
|
|||||||
|
|
||||||
// Public (no JWT)
|
// Public (no JWT)
|
||||||
adminV1.POST("/auth/login", admin.Ctrl.Login)
|
adminV1.POST("/auth/login", admin.Ctrl.Login)
|
||||||
|
adminV1.GET("/auth/init-password", admin.Ctrl.GetInitialPassword)
|
||||||
|
adminV1.GET("/auth/init-password-hash", admin.Ctrl.GetInitialPasswordHash)
|
||||||
|
|
||||||
// Authenticated
|
// Authenticated
|
||||||
authed := adminV1.Group("", middleware.CheckAdminJWT())
|
authed := adminV1.Group("", middleware.CheckAdminJWT())
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/assimon/luuu/util/sign"
|
"github.com/GMWalletApp/epusdt/util/sign"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package task
|
package task
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum"
|
"github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
@@ -18,8 +18,6 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/ethclient"
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
const bscDefaultWsURL = "wss://bsc.drpc.org"
|
|
||||||
|
|
||||||
type bscRecipientSnapshot struct {
|
type bscRecipientSnapshot struct {
|
||||||
addrs map[string]struct{}
|
addrs map[string]struct{}
|
||||||
}
|
}
|
||||||
@@ -68,7 +66,10 @@ func runBscListener(contracts []common.Address) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wsURL := resolveChainWsURL(mdb.NetworkBsc, bscDefaultWsURL)
|
wsURL, ok := resolveChainWsURL(mdb.NetworkBsc, "[BSC-WS]")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Sugar.Infof("[BSC-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts))
|
log.Sugar.Infof("[BSC-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts))
|
||||||
|
|
||||||
query := ethereum.FilterQuery{
|
query := ethereum.FilterQuery{
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
)
|
)
|
||||||
@@ -90,12 +90,22 @@ func loadChainTokenContracts(network, logPrefix string) []common.Address {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveChainWsURL picks a healthy WS endpoint from rpc_nodes for the
|
// resolveChainWsURL picks a healthy WS endpoint from rpc_nodes for the
|
||||||
// given network, falling back to the provided default (usually the
|
// given network. If no enabled node is configured, the caller skips the
|
||||||
// public node URL) when no row is configured.
|
// current listener run so admin-side disabled/deleted rows are respected.
|
||||||
func resolveChainWsURL(network, fallback string) string {
|
func resolveChainWsURL(network, logPrefix string) (string, bool) {
|
||||||
node, err := data.SelectRpcNode(network, mdb.RpcNodeTypeWs)
|
node, err := data.SelectRpcNode(network, mdb.RpcNodeTypeWs)
|
||||||
if err == nil && node != nil && node.ID > 0 {
|
if err == nil && node != nil && node.ID > 0 {
|
||||||
return node.Url
|
rpcURL := strings.TrimSpace(node.Url)
|
||||||
|
if rpcURL != "" {
|
||||||
|
return rpcURL, true
|
||||||
|
}
|
||||||
|
log.Sugar.Errorf("%s rpc_nodes id=%d has empty url", logPrefix, node.ID)
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
return fallback
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("%s resolve rpc_nodes err=%v", logPrefix, err)
|
||||||
|
} else {
|
||||||
|
log.Sugar.Warnf("%s no enabled %s WS RPC node configured in rpc_nodes", logPrefix, network)
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveChainWsURLRequiresEnabledRpcNode(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if got, ok := resolveChainWsURL(mdb.NetworkEthereum, "[TEST]"); ok {
|
||||||
|
t.Fatalf("resolveChainWsURL() = (%q, true), want false", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveChainWsURLWithRow(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
node := &mdb.RpcNode{
|
||||||
|
Network: mdb.NetworkEthereum,
|
||||||
|
Url: " wss://ethereum.example.com ",
|
||||||
|
Type: mdb.RpcNodeTypeWs,
|
||||||
|
Weight: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Status: mdb.RpcNodeStatusOk,
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Create(node).Error; err != nil {
|
||||||
|
t.Fatalf("seed rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok := resolveChainWsURL(mdb.NetworkEthereum, "[TEST]")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("resolveChainWsURL() ok=false, want true")
|
||||||
|
}
|
||||||
|
if got != "wss://ethereum.example.com" {
|
||||||
|
t.Fatalf("resolveChainWsURL() = %q, want wss://ethereum.example.com", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveChainWsURLDisabledRow(t *testing.T) {
|
||||||
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
node := &mdb.RpcNode{
|
||||||
|
Network: mdb.NetworkEthereum,
|
||||||
|
Url: "wss://disabled.example.com",
|
||||||
|
Type: mdb.RpcNodeTypeWs,
|
||||||
|
Weight: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Status: mdb.RpcNodeStatusOk,
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Create(node).Error; err != nil {
|
||||||
|
t.Fatalf("seed rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
if err := dao.Mdb.Model(node).Update("enabled", false).Error; err != nil {
|
||||||
|
t.Fatalf("disable rpc_node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, ok := resolveChainWsURL(mdb.NetworkEthereum, "[TEST]"); ok {
|
||||||
|
t.Fatalf("resolveChainWsURL() = (%q, true), want false", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-23
@@ -7,11 +7,10 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
"github.com/assimon/luuu/util/log"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum"
|
"github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
@@ -22,22 +21,6 @@ import (
|
|||||||
// Transfer 事件签名 — ERC-20 signature, same on every EVM chain.
|
// Transfer 事件签名 — ERC-20 signature, same on every EVM chain.
|
||||||
var transferEventHash = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
|
var transferEventHash = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
|
||||||
|
|
||||||
const ethereumDefaultWsURL = "wss://ethereum.publicnode.com"
|
|
||||||
|
|
||||||
// resolveEthereumWsURL picks a healthy WS endpoint. Falls back to the
|
|
||||||
// legacy .env value (if set) before the shared default — keeps existing
|
|
||||||
// deployments working until the admin adds an rpc_nodes row.
|
|
||||||
func resolveEthereumWsURL() string {
|
|
||||||
node, err := data.SelectRpcNode(mdb.NetworkEthereum, mdb.RpcNodeTypeWs)
|
|
||||||
if err == nil && node != nil && node.ID > 0 {
|
|
||||||
return node.Url
|
|
||||||
}
|
|
||||||
if u := config.GetEthereumWsUrl(); u != "" {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
return ethereumDefaultWsURL
|
|
||||||
}
|
|
||||||
|
|
||||||
type ethRecipientSnapshot struct {
|
type ethRecipientSnapshot struct {
|
||||||
addrs map[string]struct{}
|
addrs map[string]struct{}
|
||||||
}
|
}
|
||||||
@@ -88,7 +71,10 @@ func runEthereumListener(contracts []common.Address) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wsURL := resolveEthereumWsURL()
|
wsURL, ok := resolveChainWsURL(mdb.NetworkEthereum, "[ETH-WS]")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Sugar.Infof("[ETH-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts))
|
log.Sugar.Infof("[ETH-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts))
|
||||||
|
|
||||||
query := ethereum.FilterQuery{
|
query := ethereum.FilterQuery{
|
||||||
@@ -145,4 +131,3 @@ func isWatchedEthRecipient(to common.Address) bool {
|
|||||||
_, ok := snap.addrs[strings.ToLower(to.Hex())]
|
_, ok := snap.addrs[strings.ToLower(to.Hex())]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum"
|
"github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum"
|
"github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
@@ -18,8 +18,6 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/ethclient"
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
const plasmaDefaultWsURL = "wss://rpc.plasma.to"
|
|
||||||
|
|
||||||
type plasmaRecipientSnapshot struct {
|
type plasmaRecipientSnapshot struct {
|
||||||
addrs map[string]struct{}
|
addrs map[string]struct{}
|
||||||
}
|
}
|
||||||
@@ -67,7 +65,10 @@ func runPlasmaListener(contracts []common.Address) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wsURL := resolveChainWsURL(mdb.NetworkPlasma, plasmaDefaultWsURL)
|
wsURL, ok := resolveChainWsURL(mdb.NetworkPlasma, "[PLASMA-WS]")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Sugar.Infof("[PLASMA-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts))
|
log.Sugar.Infof("[PLASMA-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts))
|
||||||
|
|
||||||
query := ethereum.FilterQuery{
|
query := ethereum.FilterQuery{
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum"
|
"github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
@@ -18,8 +18,6 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/ethclient"
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
const polygonDefaultWsURL = "wss://polygon-bor-rpc.publicnode.com"
|
|
||||||
|
|
||||||
type polygonRecipientSnapshot struct {
|
type polygonRecipientSnapshot struct {
|
||||||
addrs map[string]struct{}
|
addrs map[string]struct{}
|
||||||
}
|
}
|
||||||
@@ -67,7 +65,10 @@ func runPolygonListener(contracts []common.Address) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wsURL := resolveChainWsURL(mdb.NetworkPolygon, polygonDefaultWsURL)
|
wsURL, ok := resolveChainWsURL(mdb.NetworkPolygon, "[POLYGON-WS]")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Sugar.Infof("[POLYGON-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts))
|
log.Sugar.Infof("[POLYGON-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts))
|
||||||
|
|
||||||
query := ethereum.FilterQuery{
|
query := ethereum.FilterQuery{
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package task
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListenSolJob struct{}
|
type ListenSolJob struct{}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package task
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/GMWalletApp/epusdt/model/service"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListenTrc20Job struct{}
|
type ListenTrc20Job struct{}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const rpcProbeTimeout = 5 * time.Second
|
const rpcProbeTimeout = 5 * time.Second
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --------------- ParseAddress ---------------
|
// --------------- ParseAddress ---------------
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/gookit/goutil/mathutil"
|
"github.com/gookit/goutil/mathutil"
|
||||||
"github.com/gookit/goutil/strutil"
|
"github.com/gookit/goutil/strutil"
|
||||||
tb "gopkg.in/telebot.v3"
|
tb "gopkg.in/telebot.v3"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/GMWalletApp/epusdt/util/log"
|
||||||
tb "gopkg.in/telebot.v3"
|
tb "gopkg.in/telebot.v3"
|
||||||
"gopkg.in/telebot.v3/middleware"
|
"gopkg.in/telebot.v3/middleware"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package telegram
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/assimon/luuu/internal/testutil"
|
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/GMWalletApp/epusdt/model/dao"
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func withTelegramEnv(token, proxy string, manage int64, fn func()) {
|
func withTelegramEnv(token, proxy string, manage int64, fn func()) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/btcsuite/btcutil/base58"
|
"github.com/btcsuite/btcutil/base58"
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/GMWalletApp/epusdt/util/constant"
|
||||||
"github.com/assimon/luuu/util/page"
|
"github.com/GMWalletApp/epusdt/util/page"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,25 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var spaFallbackDenyPrefixes = []string{
|
||||||
|
"/api",
|
||||||
|
"/admin/api",
|
||||||
|
"/payments",
|
||||||
|
"/pay",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldSkipSPAFallback reports whether a request path should bypass
|
||||||
|
// server-side SPA fallback to index.html and continue normal backend routing.
|
||||||
|
func ShouldSkipSPAFallback(requestPath string) bool {
|
||||||
|
for _, prefix := range spaFallbackDenyPrefixes {
|
||||||
|
if requestPath == prefix || strings.HasPrefix(requestPath, prefix+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// ResolveSPAFilePath normalizes a wildcard SPA path and maps it under wwwRoot.
|
// ResolveSPAFilePath normalizes a wildcard SPA path and maps it under wwwRoot.
|
||||||
// It strips any leading slash and blocks path traversal outside wwwRoot.
|
// It strips any leading slash and blocks path traversal outside wwwRoot.
|
||||||
// The second return value indicates whether the caller should try os.Stat
|
// The second return value indicates whether the caller should try os.Stat
|
||||||
|
|||||||
@@ -66,3 +66,32 @@ func TestResolveSPAFilePath(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldSkipSPAFallback(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "api root", path: "/api", want: true},
|
||||||
|
{name: "api sub path", path: "/api/install/defaults", want: true},
|
||||||
|
{name: "admin api root", path: "/admin/api", want: true},
|
||||||
|
{name: "admin api sub path", path: "/admin/api/v1/orders", want: true},
|
||||||
|
{name: "payments root", path: "/payments", want: true},
|
||||||
|
{name: "payments sub path", path: "/payments/gmpay/v1/order/create-transaction", want: true},
|
||||||
|
{name: "pay root", path: "/pay", want: true},
|
||||||
|
{name: "pay sub path", path: "/pay/checkout-counter/abc", want: true},
|
||||||
|
{name: "not exact prefix", path: "/apiary", want: false},
|
||||||
|
{name: "normal spa route", path: "/install", want: false},
|
||||||
|
{name: "normal asset route", path: "/assets/index.js", want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := ShouldSkipSPAFallback(tt.path)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("ShouldSkipSPAFallback(%q) = %v, want %v", tt.path, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/GMWalletApp/epusdt/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ package log
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/assimon/luuu/config"
|
"github.com/GMWalletApp/epusdt/config"
|
||||||
"github.com/natefinch/lumberjack"
|
"github.com/natefinch/lumberjack"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package sign
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/assimon/luuu/util/json"
|
"github.com/GMWalletApp/epusdt/util/json"
|
||||||
"github.com/gookit/goutil/strutil"
|
"github.com/gookit/goutil/strutil"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
import{t as e}from"./unauthorized-error-BlVgwebo.js";var t=e;export{t as component};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{t as e}from"./unauthorized-error-CxmfHb-a.js";var t=e;export{t as component};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{t as e}from"./forbidden-BEz-zjy_.js";var t=e;export{t as component};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{t as e}from"./forbidden-C7g3z0HN.js";var t=e;export{t as component};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{t as e}from"./not-found-error-BKAx_mEr.js";var t=e;export{t as component};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{t as e}from"./not-found-error-toRsO1Fi.js";var t=e;export{t as component};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{t as e}from"./general-error-CAIDyDhH.js";var t=e;export{t as component};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{t as e}from"./general-error-DKtbS-qh.js";var t=e;export{t as component};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{t as e}from"./maintenance-error-PO6kK4Oj.js";var t=e;export{t as component};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{t as e}from"./maintenance-error-vtpJpThI.js";var t=e;export{t as component};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{nu as e,ou as t,ru as n}from"./messages-BovT1T-w.js";import{o as r,s as i,t as a}from"./useRouter-D03zSgJO.js";import{t as o}from"./useStore-1-QVtiLb.js";import{f as s}from"./ClientOnly-CtYi2y5F.js";import"./redirect-dE0wk0WH.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-B8-zI9ZJ.js";import{n as p}from"./matchContext-BcwcZvp2.js";import{t as m}from"./atom-DI1Bn-1s.js";function h(e){if(typeof document<`u`&&document.querySelector){let t=e.stores.location.get(),n=t.state.__hashScrollIntoViewOptions??!0;if(n&&t.hash!==``){let e=document.getElementById(t.hash);e&&e.scrollIntoView(n)}}}var g=t(n(),1);function _(){let e=a(),t=g.useRef({router:e,mounted:!1}),[n,c]=g.useState(!1),l=o(e.stores.isLoading,e=>e),u=o(e.stores.hasPending,e=>e),f=i(l),p=l||n||u,_=i(p),v=l||u,y=i(v);return e.startTransition=e=>{c(!0),g.startTransition(()=>{e(),c(!1)})},g.useEffect(()=>{let t=e.history.subscribe(e.load),n=e.buildLocation({to:e.latestLocation.pathname,search:!0,params:!0,hash:!0,state:!0,_includeValidateSearch:!0});return s(e.latestLocation.publicHref)!==s(n.publicHref)&&e.commitLocation({...n,replace:!0}),()=>{t()}},[e,e.history]),r(()=>{typeof window<`u`&&e.ssr||t.current.router===e&&t.current.mounted||(t.current={router:e,mounted:!0},(async()=>{try{await e.load()}catch(e){console.error(e)}})())},[e]),r(()=>{f&&!l&&e.emit({type:`onLoad`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[f,e,l]),r(()=>{y&&!v&&e.emit({type:`onBeforeRouteMount`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[v,y,e]),r(()=>{if(_&&!p){let t=d(e.stores.location.get(),e.stores.resolvedLocation.get());e.emit({type:`onResolved`,...t}),m(()=>{e.stores.status.set(`idle`),e.stores.resolvedLocation.set(e.stores.location.get())}),t.hrefChanged&&h(e)}},[p,_,e]),null}var v=e();function y(){let e=a(),t=e.routesById.__root__.options.pendingComponent??e.options.defaultPendingComponent,n=t?(0,v.jsx)(t,{}):null,r=(0,v.jsxs)(typeof document<`u`&&e.ssr?u:g.Suspense,{fallback:n,children:[(0,v.jsx)(_,{}),(0,v.jsx)(b,{})]});return e.options.InnerWrap?(0,v.jsx)(e.options.InnerWrap,{children:r}):r}function b(){let e=a(),t=o(e.stores.firstId,e=>e),n=o(e.stores.loadedAt,e=>e),r=t?(0,v.jsx)(f,{matchId:t}):null;return(0,v.jsx)(p.Provider,{value:t,children:e.options.disableGlobalCatchBoundary?r:(0,v.jsx)(l,{getResetKey:()=>n,errorComponent:c,onCatch:void 0,children:r})})}function x(){let e=a();return o(e.stores.matchRouteDeps,e=>e),g.useCallback(t=>{let{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a,...o}=t;return e.matchRoute(o,{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a})},[e])}export{x as n,y as t};
|
import{Eu as e,Tu as t,ku as n}from"./messages-xqHXgrWc.js";import{o as r,s as i,t as a}from"./useRouter-DbizRCmt.js";import{t as o}from"./useStore-8Xlx4tji.js";import{f as s}from"./ClientOnly-BerhZEDB.js";import"./redirect-dE0wk0WH.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-DWfHsy4k.js";import{n as p}from"./matchContext-DOaBJYLs.js";import{t as m}from"./atom-DI1Bn-1s.js";function h(e){if(typeof document<`u`&&document.querySelector){let t=e.stores.location.get(),n=t.state.__hashScrollIntoViewOptions??!0;if(n&&t.hash!==``){let e=document.getElementById(t.hash);e&&e.scrollIntoView(n)}}}var g=n(e(),1);function _(){let e=a(),t=g.useRef({router:e,mounted:!1}),[n,c]=g.useState(!1),l=o(e.stores.isLoading,e=>e),u=o(e.stores.hasPending,e=>e),f=i(l),p=l||n||u,_=i(p),v=l||u,y=i(v);return e.startTransition=e=>{c(!0),g.startTransition(()=>{e(),c(!1)})},g.useEffect(()=>{let t=e.history.subscribe(e.load),n=e.buildLocation({to:e.latestLocation.pathname,search:!0,params:!0,hash:!0,state:!0,_includeValidateSearch:!0});return s(e.latestLocation.publicHref)!==s(n.publicHref)&&e.commitLocation({...n,replace:!0}),()=>{t()}},[e,e.history]),r(()=>{typeof window<`u`&&e.ssr||t.current.router===e&&t.current.mounted||(t.current={router:e,mounted:!0},(async()=>{try{await e.load()}catch(e){console.error(e)}})())},[e]),r(()=>{f&&!l&&e.emit({type:`onLoad`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[f,e,l]),r(()=>{y&&!v&&e.emit({type:`onBeforeRouteMount`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[v,y,e]),r(()=>{if(_&&!p){let t=d(e.stores.location.get(),e.stores.resolvedLocation.get());e.emit({type:`onResolved`,...t}),m(()=>{e.stores.status.set(`idle`),e.stores.resolvedLocation.set(e.stores.location.get())}),t.hrefChanged&&h(e)}},[p,_,e]),null}var v=t();function y(){let e=a(),t=e.routesById.__root__.options.pendingComponent??e.options.defaultPendingComponent,n=t?(0,v.jsx)(t,{}):null,r=(0,v.jsxs)(typeof document<`u`&&e.ssr?u:g.Suspense,{fallback:n,children:[(0,v.jsx)(_,{}),(0,v.jsx)(b,{})]});return e.options.InnerWrap?(0,v.jsx)(e.options.InnerWrap,{children:r}):r}function b(){let e=a(),t=o(e.stores.firstId,e=>e),n=o(e.stores.loadedAt,e=>e),r=t?(0,v.jsx)(f,{matchId:t}):null;return(0,v.jsx)(p.Provider,{value:t,children:e.options.disableGlobalCatchBoundary?r:(0,v.jsx)(l,{getResetKey:()=>n,errorComponent:c,onCatch:void 0,children:r})})}function x(){let e=a();return o(e.stores.matchRouteDeps,e=>e),g.useCallback(t=>{let{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a,...o}=t;return e.matchRoute(o,{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a})},[e])}export{x as n,y as t};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user