mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5168beda00 | |||
| 5e4d5dfae4 | |||
| 32ca778735 | |||
| 730ff983f8 | |||
| 93b32d38d9 | |||
| 3f071e6c01 | |||
| a7963b335b | |||
| d7fa90112d |
@@ -61,3 +61,11 @@ order_notice_max_retry=0
|
|||||||
forced_usdt_rate=
|
forced_usdt_rate=
|
||||||
api_rate_url=
|
api_rate_url=
|
||||||
tron_grid_api_key=
|
tron_grid_api_key=
|
||||||
|
solana_rpc_url=
|
||||||
|
|
||||||
|
ethereum_ws_url=wss://ethereum.publicnode.com
|
||||||
|
|
||||||
|
|
||||||
|
# epay
|
||||||
|
epay_pid=
|
||||||
|
epay_key=
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist/
|
dist/
|
||||||
|
*.log
|
||||||
@@ -328,3 +328,23 @@ 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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEpayPid() int {
|
||||||
|
return viper.GetInt("epay_pid")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEpayKey() string {
|
||||||
|
return viper.GetString("epay_key")
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,8 +67,27 @@ func TestResolveConfigFilePathUsesCurrentDirectoryByDefault(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("resolve config path: %v", err)
|
t.Fatalf("resolve config path: %v", err)
|
||||||
}
|
}
|
||||||
if got != configPath {
|
|
||||||
t.Fatalf("config path = %s, want %s", got, configPath)
|
gotAbs, err := filepath.Abs(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("abs got: %v", err)
|
||||||
|
}
|
||||||
|
wantAbs, err := filepath.Abs(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("abs want: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotReal, err := filepath.EvalSymlinks(gotAbs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("eval symlinks got: %v", err)
|
||||||
|
}
|
||||||
|
wantReal, err := filepath.EvalSymlinks(wantAbs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("eval symlinks want: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotReal != wantReal {
|
||||||
|
t.Fatalf("config path = %s, want %s", gotReal, wantReal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package comm
|
package comm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/assimon/luuu/model/request"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/assimon/luuu/model/service"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/assimon/luuu/util/constant"
|
||||||
@@ -22,3 +26,53 @@ func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
|
|||||||
}
|
}
|
||||||
return c.SucJson(ctx, resp)
|
return c.SucJson(ctx, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SwitchNetwork 切换支付网络,创建或返回子订单
|
||||||
|
func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
||||||
|
req := new(request.SwitchNetworkRequest)
|
||||||
|
if err = ctx.Bind(req); err != nil {
|
||||||
|
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
|
}
|
||||||
|
if err = c.ValidateStruct(ctx, req); err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
resp, err := service.SwitchNetwork(req)
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := json.MarshalIndent(resp, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("switch network response: \n%s", string(jsonBytes))
|
||||||
|
|
||||||
|
return c.SucJson(ctx, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BaseCommController) CreateTransactionAndRedirect(ctx echo.Context) (err error) {
|
||||||
|
req := new(request.CreateTransactionRequest)
|
||||||
|
if err = ctx.Bind(req); err != nil {
|
||||||
|
log.Println("bind request error:", err)
|
||||||
|
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
|
}
|
||||||
|
if err = c.ValidateStruct(ctx, req); err != nil {
|
||||||
|
log.Println("validate request error:", err)
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
resp, err := service.CreateTransaction(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("create transaction error:", err)
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("create transaction response: %+v\n", resp)
|
||||||
|
|
||||||
|
tradeID := resp.TradeId
|
||||||
|
|
||||||
|
ctx.Redirect(302, "/pay/checkout-counter/"+tradeID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package comm
|
package comm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -30,7 +32,13 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.String(http.StatusOK, err.Error())
|
return ctx.String(http.StatusOK, err.Error())
|
||||||
}
|
}
|
||||||
resp.Network = "TRON"
|
|
||||||
|
jsonByte, err := json.MarshalIndent(resp, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return ctx.String(http.StatusOK, err.Error())
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", string(jsonByte))
|
||||||
|
|
||||||
return tmpl.Execute(ctx.Response(), resp)
|
return tmpl.Execute(ctx.Response(), resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package comm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/assimon/luuu/model/data"
|
||||||
|
"github.com/assimon/luuu/model/mdb"
|
||||||
|
"github.com/assimon/luuu/util/constant"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addWalletRequest struct {
|
||||||
|
Network string `json:"network" validate:"required"`
|
||||||
|
Address string `json:"address" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type changeStatusRequest struct {
|
||||||
|
Status int `json:"status" validate:"required|in:1,2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWallet 添加钱包地址
|
||||||
|
func (c *BaseCommController) AddWallet(ctx echo.Context) error {
|
||||||
|
req := new(addWalletRequest)
|
||||||
|
if err := ctx.Bind(req); err != nil {
|
||||||
|
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
|
}
|
||||||
|
if err := c.ValidateStruct(ctx, req); err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
wallet, err := data.AddWalletAddressWithNetwork(req.Network, req.Address)
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
return c.SucJson(ctx, wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListWallets 获取钱包列表
|
||||||
|
func (c *BaseCommController) ListWallets(ctx echo.Context) error {
|
||||||
|
network := ctx.QueryParam("network")
|
||||||
|
var wallets []mdb.WalletAddress
|
||||||
|
var err error
|
||||||
|
if network != "" {
|
||||||
|
wallets, err = data.GetAllWalletAddressByNetwork(network)
|
||||||
|
} else {
|
||||||
|
wallets, err = data.GetAllWalletAddress()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
return c.SucJson(ctx, wallets)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWallet 获取单个钱包
|
||||||
|
func (c *BaseCommController) GetWallet(ctx echo.Context) error {
|
||||||
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
|
}
|
||||||
|
wallet, err := data.GetWalletAddressById(id)
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
if wallet.ID <= 0 {
|
||||||
|
return c.FailJson(ctx, constant.NotAvailableWalletAddress)
|
||||||
|
}
|
||||||
|
return c.SucJson(ctx, wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeWalletStatus 启用/禁用钱包
|
||||||
|
func (c *BaseCommController) ChangeWalletStatus(ctx echo.Context) error {
|
||||||
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
|
}
|
||||||
|
req := new(changeStatusRequest)
|
||||||
|
if err := ctx.Bind(req); err != nil {
|
||||||
|
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
|
}
|
||||||
|
if err := c.ValidateStruct(ctx, req); err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
if err := data.ChangeWalletAddressStatus(id, req.Status); err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
return c.SucJson(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWallet 删除钱包
|
||||||
|
func (c *BaseCommController) DeleteWallet(ctx echo.Context) error {
|
||||||
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
|
}
|
||||||
|
if err := data.DeleteWalletAddressById(id); err != nil {
|
||||||
|
return c.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
return c.SucJson(ctx, nil)
|
||||||
|
}
|
||||||
+22
-3
@@ -5,6 +5,7 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcutil v1.0.2
|
github.com/btcsuite/btcutil v1.0.2
|
||||||
github.com/dromara/carbon/v2 v2.6.15
|
github.com/dromara/carbon/v2 v2.6.15
|
||||||
|
github.com/ethereum/go-ethereum v1.15.11
|
||||||
github.com/gagliardetto/solana-go v1.16.0
|
github.com/gagliardetto/solana-go v1.16.0
|
||||||
github.com/go-resty/resty/v2 v2.11.0
|
github.com/go-resty/resty/v2 v2.11.0
|
||||||
github.com/gookit/color v1.5.0
|
github.com/gookit/color v1.5.0
|
||||||
@@ -32,19 +33,32 @@ require (
|
|||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0 // indirect
|
||||||
github.com/blendle/zapdriver v1.3.1 // indirect
|
github.com/blendle/zapdriver v1.3.1 // indirect
|
||||||
|
github.com/consensys/bavard v0.1.27 // indirect
|
||||||
|
github.com/consensys/gnark-crypto v0.16.0 // indirect
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect
|
||||||
|
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect
|
||||||
|
github.com/ethereum/go-verkle v0.2.2 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/gagliardetto/binary v0.8.0 // indirect
|
github.com/gagliardetto/binary v0.8.0 // indirect
|
||||||
github.com/gagliardetto/treeout v0.1.4 // indirect
|
github.com/gagliardetto/treeout v0.1.4 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gookit/filter v1.1.2 // indirect
|
github.com/gookit/filter v1.1.2 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/holiman/uint256 v1.3.2 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
@@ -59,6 +73,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||||
|
github.com/mmcloughlin/addchain v0.4.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
|
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
|
||||||
@@ -67,30 +82,34 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.14.0 // indirect
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/streamingfast/logging v0.0.0-20250404134358-92b15d2fbd2e // indirect
|
github.com/streamingfast/logging v0.0.0-20250404134358-92b15d2fbd2e // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/supranational/blst v0.3.14 // indirect
|
||||||
github.com/tidwall/match v1.2.0 // indirect
|
github.com/tidwall/match v1.2.0 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/ratelimit v0.3.1 // indirect
|
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/term v0.39.0 // indirect
|
golang.org/x/term v0.39.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.47.0 // indirect
|
modernc.org/sqlite v1.47.0 // indirect
|
||||||
|
rsc.io/tmplfunc v0.0.3 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+148
-15
@@ -1,13 +1,21 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
|
||||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||||
|
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||||
|
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||||
|
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
|
||||||
|
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
|
||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE=
|
github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE=
|
||||||
github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc=
|
github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc=
|
||||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
@@ -20,15 +28,52 @@ github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVa
|
|||||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
|
||||||
|
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
|
||||||
|
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
|
||||||
|
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||||
|
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
||||||
|
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||||
|
github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
|
||||||
|
github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
|
||||||
|
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
||||||
|
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||||
|
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||||
|
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||||
|
github.com/consensys/bavard v0.1.27 h1:j6hKUrGAy/H+gpNrpLU3I26n1yc+VMGmd6ID5+gAhOs=
|
||||||
|
github.com/consensys/bavard v0.1.27/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs=
|
||||||
|
github.com/consensys/gnark-crypto v0.16.0 h1:8Dl4eYmUWK9WmlP1Bj6je688gBRJCJbT8Mw4KoTAawo=
|
||||||
|
github.com/consensys/gnark-crypto v0.16.0/go.mod h1:Ke3j06ndtPTVvo++PhGNgvm+lgpLvzbcE2MqljY7diU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI=
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
|
||||||
|
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
|
||||||
|
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
|
||||||
|
github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4=
|
||||||
|
github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
|
||||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
github.com/dromara/carbon/v2 v2.6.15 h1:3HuC3XcWczIHUTbg/f0CSVydtKEdM+P0GM1sdsbwXmI=
|
github.com/dromara/carbon/v2 v2.6.15 h1:3HuC3XcWczIHUTbg/f0CSVydtKEdM+P0GM1sdsbwXmI=
|
||||||
github.com/dromara/carbon/v2 v2.6.15/go.mod h1:NGo3reeV5vhWCYWcSqbJRZm46MEwyfYI5EJRdVFoLJo=
|
github.com/dromara/carbon/v2 v2.6.15/go.mod h1:NGo3reeV5vhWCYWcSqbJRZm46MEwyfYI5EJRdVFoLJo=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w=
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E=
|
||||||
|
github.com/ethereum/go-ethereum v1.15.11 h1:JK73WKeu0WC0O1eyX+mdQAVHUV+UR1a9VB/domDngBU=
|
||||||
|
github.com/ethereum/go-ethereum v1.15.11/go.mod h1:mf8YiHIb0GR4x4TipcvBUPxJLw1mFdmxzoDi11sDRoI=
|
||||||
|
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
|
||||||
|
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
|
||||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
@@ -44,6 +89,11 @@ github.com/gagliardetto/solana-go v1.16.0 h1:lRPn/NxVmxzXw+vQ3AxH33jQIvj8avx2CKV
|
|||||||
github.com/gagliardetto/solana-go v1.16.0/go.mod h1:2n7osXNoDeUhq1r1lOgCMVkl90yYUVrV9FHGINBWPHU=
|
github.com/gagliardetto/solana-go v1.16.0/go.mod h1:2n7osXNoDeUhq1r1lOgCMVkl90yYUVrV9FHGINBWPHU=
|
||||||
github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw=
|
github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw=
|
||||||
github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok=
|
github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok=
|
||||||
|
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||||
|
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||||
|
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
@@ -55,14 +105,25 @@ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
|
|||||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
||||||
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
|
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
|
||||||
|
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gookit/color v1.3.8/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ=
|
github.com/gookit/color v1.3.8/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ=
|
||||||
@@ -76,9 +137,21 @@ github.com/gookit/goutil v0.4.6 h1:LSATnmIyR0rV4BMj3XyaKFYtX/HbRURnPVHt6Zb5ABs=
|
|||||||
github.com/gookit/goutil v0.4.6/go.mod h1:pq1eTibwb2wN96jrci0xy7xogWzzo9CihOQJEAvz4yQ=
|
github.com/gookit/goutil v0.4.6/go.mod h1:pq1eTibwb2wN96jrci0xy7xogWzzo9CihOQJEAvz4yQ=
|
||||||
github.com/gookit/validate v1.3.1 h1:YoxBxG5H+WucKjfEo8X+QvYOzED7ue3Mfpq6jtpIbYs=
|
github.com/gookit/validate v1.3.1 h1:YoxBxG5H+WucKjfEo8X+QvYOzED7ue3Mfpq6jtpIbYs=
|
||||||
github.com/gookit/validate v1.3.1/go.mod h1:mzcr3U5XtAlQVAgbW0wbmDGiuefU/MNuDkXFMBNb1ko=
|
github.com/gookit/validate v1.3.1/go.mod h1:mzcr3U5XtAlQVAgbW0wbmDGiuefU/MNuDkXFMBNb1ko=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
|
||||||
|
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
|
||||||
|
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
|
||||||
|
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
|
||||||
|
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
|
||||||
|
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
|
||||||
|
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||||
|
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
@@ -87,6 +160,8 @@ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/
|
|||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
|
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
|
||||||
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
||||||
|
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||||
|
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
@@ -108,10 +183,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/labstack/echo/v4 v4.6.0 h1:vsYEeeYy077cB5yMpgI+ubA7iVRZEtrzHhcvRhd27gA=
|
github.com/labstack/echo/v4 v4.6.0 h1:vsYEeeYy077cB5yMpgI+ubA7iVRZEtrzHhcvRhd27gA=
|
||||||
github.com/labstack/echo/v4 v4.6.0/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
|
github.com/labstack/echo/v4 v4.6.0/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
|
||||||
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
||||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||||
|
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||||
|
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/libtnb/sqlite v1.0.1 h1:H4kdU3LBOjdxfQy8s02/wG1rNmp8GMiqzgel0axewtI=
|
github.com/libtnb/sqlite v1.0.1 h1:H4kdU3LBOjdxfQy8s02/wG1rNmp8GMiqzgel0axewtI=
|
||||||
github.com/libtnb/sqlite v1.0.1/go.mod h1:zuagCP0nvIJwr4e0kdzq9Yc2Pw5HlVOoLT6ZmzmjOes=
|
github.com/libtnb/sqlite v1.0.1/go.mod h1:zuagCP0nvIJwr4e0kdzq9Yc2Pw5HlVOoLT6ZmzmjOes=
|
||||||
@@ -129,12 +208,23 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
|||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
|
||||||
|
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
|
||||||
|
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
|
||||||
|
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
|
||||||
|
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -150,22 +240,47 @@ github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4
|
|||||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||||
|
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
|
||||||
|
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
|
||||||
|
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
||||||
|
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||||
|
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||||
|
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
|
||||||
|
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||||
|
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
|
||||||
|
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
|
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||||
|
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||||
|
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||||
|
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||||
|
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||||
@@ -173,6 +288,8 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
|||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs=
|
github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs=
|
||||||
github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I=
|
github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I=
|
||||||
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
|
||||||
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
@@ -202,6 +319,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo=
|
||||||
|
github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||||
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
|
||||||
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
|
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
|
||||||
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
|
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
@@ -212,6 +333,12 @@ github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
|||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
|
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||||
|
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
@@ -219,14 +346,14 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
|
|||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
|
||||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
@@ -234,8 +361,6 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
|
|||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
|
||||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
@@ -252,6 +377,8 @@ golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0
|
|||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
@@ -284,6 +411,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -298,9 +426,11 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
@@ -336,15 +466,16 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
|||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo=
|
gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo=
|
||||||
gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg=
|
gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
@@ -394,3 +525,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
|
||||||
|
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/assimon/luuu/config"
|
||||||
|
"github.com/assimon/luuu/util/constant"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckApiToken validates the Authorization header against the configured api_auth_token.
|
||||||
|
// Use this for management APIs (GET/POST) where body-based signature is not practical.
|
||||||
|
func CheckApiToken() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
token := ctx.Request().Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
token = ctx.QueryParam("api_token")
|
||||||
|
}
|
||||||
|
if token == "" || token != config.GetApiAuthToken() {
|
||||||
|
return constant.SignatureErr
|
||||||
|
}
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,9 @@ func RuntimeInit() error {
|
|||||||
if err = RuntimeDB.Exec("DROP INDEX IF EXISTS transaction_lock_token_amount_uindex").Error; err != nil {
|
if err = RuntimeDB.Exec("DROP INDEX IF EXISTS transaction_lock_token_amount_uindex").Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = RuntimeDB.Exec("DROP INDEX IF EXISTS transaction_lock_address_token_amount_uindex").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err = RuntimeDB.AutoMigrate(&mdb.TransactionLock{}); err != nil {
|
if err = RuntimeDB.AutoMigrate(&mdb.TransactionLock{}); err != nil {
|
||||||
color.Red.Printf("[runtime_db] sqlite migrate DB(TransactionLock),err=%s\n", err)
|
color.Red.Printf("[runtime_db] sqlite migrate DB(TransactionLock),err=%s\n", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -108,11 +108,103 @@ func UpdateOrderIsExpirationById(id uint64, expirationCutoff time.Time) (bool, e
|
|||||||
return result.RowsAffected > 0, result.Error
|
return result.RowsAffected > 0, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTradeIdByWalletAddressAndAmountAndToken resolves the reserved trade id by address, token and amount.
|
// CountActiveSubOrders counts sub-orders with status=WaitPay under a parent.
|
||||||
func GetTradeIdByWalletAddressAndAmountAndToken(address string, token string, amount float64) (string, error) {
|
func CountActiveSubOrders(parentTradeId string) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("parent_trade_id = ?", parentTradeId).
|
||||||
|
Where("status = ?", mdb.StatusWaitPay).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubOrderByTokenNetwork finds an existing active sub-order matching token+network under a parent.
|
||||||
|
func GetSubOrderByTokenNetwork(parentTradeId string, token string, network string) (*mdb.Orders, error) {
|
||||||
|
order := new(mdb.Orders)
|
||||||
|
err := dao.Mdb.Model(order).
|
||||||
|
Where("parent_trade_id = ?", parentTradeId).
|
||||||
|
Where("token = ?", token).
|
||||||
|
Where("network = ?", network).
|
||||||
|
Where("status = ?", mdb.StatusWaitPay).
|
||||||
|
Limit(1).
|
||||||
|
Find(order).Error
|
||||||
|
return order, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSiblingSubOrders returns active sub-orders under the same parent, excluding the given trade_id.
|
||||||
|
func GetSiblingSubOrders(parentTradeId string, excludeTradeId string) ([]mdb.Orders, error) {
|
||||||
|
var orders []mdb.Orders
|
||||||
|
err := dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("parent_trade_id = ?", parentTradeId).
|
||||||
|
Where("trade_id != ?", excludeTradeId).
|
||||||
|
Where("status = ?", mdb.StatusWaitPay).
|
||||||
|
Find(&orders).Error
|
||||||
|
return orders, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkParentOrderSuccess updates the parent order with the sub-order's payment details.
|
||||||
|
// Token and network are NOT overwritten — the parent keeps its original values.
|
||||||
|
func MarkParentOrderSuccess(parentTradeId string, sub *mdb.Orders) (bool, error) {
|
||||||
|
result := dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", parentTradeId).
|
||||||
|
Where("status = ?", mdb.StatusWaitPay).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"status": mdb.StatusPaySuccess,
|
||||||
|
"block_transaction_id": sub.BlockTransactionId,
|
||||||
|
"callback_confirm": mdb.CallBackConfirmNo,
|
||||||
|
"actual_amount": sub.ActualAmount,
|
||||||
|
"receive_address": sub.ReceiveAddress,
|
||||||
|
})
|
||||||
|
return result.RowsAffected > 0, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkOrderSelected sets is_selected=true for the given trade_id.
|
||||||
|
func MarkOrderSelected(tradeId string) error {
|
||||||
|
return dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", tradeId).
|
||||||
|
Update("is_selected", true).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshOrderExpiration resets created_at to now so the expiration timer restarts.
|
||||||
|
// Called on the parent order when a sub-order is created or returned.
|
||||||
|
func RefreshOrderExpiration(tradeId string) error {
|
||||||
|
return dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", tradeId).
|
||||||
|
Update("created_at", time.Now()).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetCallbackConfirmOk sets callback_confirm back to Ok.
|
||||||
|
// Prevents the callback worker from retrying a sub-order with an empty notify_url.
|
||||||
|
func ResetCallbackConfirmOk(tradeId string) error {
|
||||||
|
return dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", tradeId).
|
||||||
|
Update("callback_confirm", mdb.CallBackConfirmOk).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveSubOrders returns all active sub-orders under a parent.
|
||||||
|
func GetActiveSubOrders(parentTradeId string) ([]mdb.Orders, error) {
|
||||||
|
var orders []mdb.Orders
|
||||||
|
err := dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("parent_trade_id = ?", parentTradeId).
|
||||||
|
Where("status = ?", mdb.StatusWaitPay).
|
||||||
|
Find(&orders).Error
|
||||||
|
return orders, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpireOrderByTradeId marks a single order as expired if still waiting.
|
||||||
|
func ExpireOrderByTradeId(tradeId string) error {
|
||||||
|
return dao.Mdb.Model(&mdb.Orders{}).
|
||||||
|
Where("trade_id = ?", tradeId).
|
||||||
|
Where("status = ?", mdb.StatusWaitPay).
|
||||||
|
Update("status", mdb.StatusExpired).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTradeIdByWalletAddressAndAmountAndToken resolves the reserved trade id by network, address, token and amount.
|
||||||
|
func GetTradeIdByWalletAddressAndAmountAndToken(network string, address string, token string, amount float64) (string, error) {
|
||||||
scaledAmount, _ := normalizeLockAmount(amount)
|
scaledAmount, _ := normalizeLockAmount(amount)
|
||||||
var lock mdb.TransactionLock
|
var lock mdb.TransactionLock
|
||||||
err := dao.RuntimeDB.Model(&mdb.TransactionLock{}).
|
err := dao.RuntimeDB.Model(&mdb.TransactionLock{}).
|
||||||
|
Where("network = ?", network).
|
||||||
Where("address = ?", address).
|
Where("address = ?", address).
|
||||||
Where("token = ?", normalizeLockToken(token)).
|
Where("token = ?", normalizeLockToken(token)).
|
||||||
Where("amount_scaled = ?", scaledAmount).
|
Where("amount_scaled = ?", scaledAmount).
|
||||||
@@ -128,12 +220,13 @@ func GetTradeIdByWalletAddressAndAmountAndToken(address string, token string, am
|
|||||||
return lock.TradeId, nil
|
return lock.TradeId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LockTransaction reserves an address+token+amount pair in sqlite until expiration.
|
// LockTransaction reserves a network+address+token+amount pair in sqlite until expiration.
|
||||||
func LockTransaction(address, token, tradeID string, amount float64, expirationTime time.Duration) error {
|
func LockTransaction(network, address, token, tradeID string, amount float64, expirationTime time.Duration) error {
|
||||||
scaledAmount, amountText := normalizeLockAmount(amount)
|
scaledAmount, amountText := normalizeLockAmount(amount)
|
||||||
normalizedToken := normalizeLockToken(token)
|
normalizedToken := normalizeLockToken(token)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
lock := &mdb.TransactionLock{
|
lock := &mdb.TransactionLock{
|
||||||
|
Network: network,
|
||||||
Address: address,
|
Address: address,
|
||||||
Token: normalizedToken,
|
Token: normalizedToken,
|
||||||
AmountScaled: scaledAmount,
|
AmountScaled: scaledAmount,
|
||||||
@@ -143,7 +236,8 @@ func LockTransaction(address, token, tradeID string, amount float64, expirationT
|
|||||||
}
|
}
|
||||||
|
|
||||||
return dao.RuntimeDB.Transaction(func(tx *gorm.DB) error {
|
return dao.RuntimeDB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Where("address = ?", address).
|
if err := tx.Where("network = ?", network).
|
||||||
|
Where("address = ?", address).
|
||||||
Where("token = ?", normalizedToken).
|
Where("token = ?", normalizedToken).
|
||||||
Where("amount_scaled = ?", scaledAmount).
|
Where("amount_scaled = ?", scaledAmount).
|
||||||
Where("expires_at <= ?", now).
|
Where("expires_at <= ?", now).
|
||||||
@@ -165,10 +259,11 @@ func LockTransaction(address, token, tradeID string, amount float64, expirationT
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnLockTransaction releases the reservation for address+token+amount.
|
// UnLockTransaction releases the reservation for network+address+token+amount.
|
||||||
func UnLockTransaction(address string, token string, amount float64) error {
|
func UnLockTransaction(network string, address string, token string, amount float64) error {
|
||||||
scaledAmount, _ := normalizeLockAmount(amount)
|
scaledAmount, _ := normalizeLockAmount(amount)
|
||||||
return dao.RuntimeDB.
|
return dao.RuntimeDB.
|
||||||
|
Where("network = ?", network).
|
||||||
Where("address = ?", address).
|
Where("address = ?", address).
|
||||||
Where("token = ?", normalizeLockToken(token)).
|
Where("token = ?", normalizeLockToken(token)).
|
||||||
Where("amount_scaled = ?", scaledAmount).
|
Where("amount_scaled = ?", scaledAmount).
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/assimon/luuu/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/assimon/luuu/model/mdb"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/assimon/luuu/util/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddWalletAddress 创建钱包
|
// AddWalletAddress 创建钱包 (默认 tron 网络,用于 Telegram 添加)
|
||||||
func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
|
func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
|
||||||
exist, err := GetWalletAddressByToken(address)
|
return AddWalletAddressWithNetwork(mdb.NetworkTron, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWalletAddressWithNetwork 创建指定网络的钱包地址
|
||||||
|
func AddWalletAddressWithNetwork(network, address string) (*mdb.WalletAddress, error) {
|
||||||
|
network = strings.ToLower(strings.TrimSpace(network))
|
||||||
|
address = strings.TrimSpace(address)
|
||||||
|
exist, err := GetWalletAddressByNetworkAndAddress(network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -16,6 +25,7 @@ func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
|
|||||||
return nil, constant.WalletAddressAlreadyExists
|
return nil, constant.WalletAddressAlreadyExists
|
||||||
}
|
}
|
||||||
walletAddress := &mdb.WalletAddress{
|
walletAddress := &mdb.WalletAddress{
|
||||||
|
Network: network,
|
||||||
Address: address,
|
Address: address,
|
||||||
Status: mdb.TokenStatusEnable,
|
Status: mdb.TokenStatusEnable,
|
||||||
}
|
}
|
||||||
@@ -23,7 +33,17 @@ func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
|
|||||||
return walletAddress, err
|
return walletAddress, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWalletAddressByToken 通过钱包地址获取address
|
// GetWalletAddressByNetworkAndAddress 通过网络和地址查询
|
||||||
|
func GetWalletAddressByNetworkAndAddress(network, address string) (*mdb.WalletAddress, error) {
|
||||||
|
walletAddress := new(mdb.WalletAddress)
|
||||||
|
err := dao.Mdb.Model(walletAddress).
|
||||||
|
Where("network = ?", network).
|
||||||
|
Where("address = ?", address).
|
||||||
|
Limit(1).Find(walletAddress).Error
|
||||||
|
return walletAddress, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWalletAddressByToken 通过钱包地址获取address (兼容旧接口)
|
||||||
func GetWalletAddressByToken(address string) (*mdb.WalletAddress, error) {
|
func GetWalletAddressByToken(address string) (*mdb.WalletAddress, error) {
|
||||||
walletAddress := new(mdb.WalletAddress)
|
walletAddress := new(mdb.WalletAddress)
|
||||||
err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, "address = ?", address).Error
|
err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, "address = ?", address).Error
|
||||||
@@ -50,6 +70,16 @@ func GetAvailableWalletAddress() ([]mdb.WalletAddress, error) {
|
|||||||
return WalletAddressList, err
|
return WalletAddressList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAvailableWalletAddressByNetwork 获得指定网络的所有可用钱包地址
|
||||||
|
func GetAvailableWalletAddressByNetwork(network string) ([]mdb.WalletAddress, error) {
|
||||||
|
var list []mdb.WalletAddress
|
||||||
|
err := dao.Mdb.Model(list).
|
||||||
|
Where("status = ?", mdb.TokenStatusEnable).
|
||||||
|
Where("network = ?", network).
|
||||||
|
Find(&list).Error
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllWalletAddress 获得所有钱包地址
|
// GetAllWalletAddress 获得所有钱包地址
|
||||||
func GetAllWalletAddress() ([]mdb.WalletAddress, error) {
|
func GetAllWalletAddress() ([]mdb.WalletAddress, error) {
|
||||||
var WalletAddressList []mdb.WalletAddress
|
var WalletAddressList []mdb.WalletAddress
|
||||||
@@ -57,6 +87,13 @@ func GetAllWalletAddress() ([]mdb.WalletAddress, error) {
|
|||||||
return WalletAddressList, err
|
return WalletAddressList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllWalletAddressByNetwork 获得指定网络的所有钱包地址
|
||||||
|
func GetAllWalletAddressByNetwork(network string) ([]mdb.WalletAddress, error) {
|
||||||
|
var list []mdb.WalletAddress
|
||||||
|
err := dao.Mdb.Model(list).Where("network = ?", network).Find(&list).Error
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
// ChangeWalletAddressStatus 启用禁用钱包
|
// ChangeWalletAddressStatus 启用禁用钱包
|
||||||
func ChangeWalletAddressStatus(id uint64, status int) error {
|
func ChangeWalletAddressStatus(id uint64, status int) error {
|
||||||
err := dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", id).Update("status", status).Error
|
err := dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", id).Update("status", status).Error
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ const (
|
|||||||
CallBackConfirmNo = 2
|
CallBackConfirmNo = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaymentTypeEpay = "Epay"
|
||||||
|
)
|
||||||
|
|
||||||
type Orders struct {
|
type Orders struct {
|
||||||
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id"`
|
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id"`
|
||||||
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id"`
|
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id"` // the order id is generated by client, and will notify client when order is paid
|
||||||
|
ParentTradeId string `gorm:"column:parent_trade_id;index:idx_orders_parent_trade_id;default:''" json:"parent_trade_id"`
|
||||||
BlockTransactionId string `gorm:"index:orders_block_transaction_id_index;column:block_transaction_id" json:"block_transaction_id"`
|
BlockTransactionId string `gorm:"index:orders_block_transaction_id_index;column:block_transaction_id" json:"block_transaction_id"`
|
||||||
Amount float64 `gorm:"column:amount" json:"amount"`
|
Amount float64 `gorm:"column:amount" json:"amount"`
|
||||||
Currency string `gorm:"column:currency" json:"currency"`
|
Currency string `gorm:"column:currency" json:"currency"`
|
||||||
@@ -21,8 +26,11 @@ type Orders struct {
|
|||||||
Status int `gorm:"column:status;default:1" json:"status"`
|
Status int `gorm:"column:status;default:1" json:"status"`
|
||||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"`
|
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"`
|
||||||
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"`
|
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"`
|
||||||
|
Name string `gorm:"column:name" json:"name"`
|
||||||
CallbackNum int `gorm:"column:callback_num;default:0" json:"callback_num"`
|
CallbackNum int `gorm:"column:callback_num;default:0" json:"callback_num"`
|
||||||
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm"`
|
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm"`
|
||||||
|
IsSelected bool `gorm:"column:is_selected;default:false" json:"is_selected"`
|
||||||
|
PaymentType string `gorm:"column:payment_type" json:"payment_type"`
|
||||||
BaseModel
|
BaseModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import "time"
|
|||||||
|
|
||||||
type TransactionLock struct {
|
type TransactionLock struct {
|
||||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
Address string `gorm:"column:address;uniqueIndex:transaction_lock_address_token_amount_uindex,priority:1" json:"address"`
|
Network string `gorm:"column:network;uniqueIndex:transaction_lock_network_address_token_amount_uindex,priority:1" json:"network"`
|
||||||
Token string `gorm:"column:token;uniqueIndex:transaction_lock_address_token_amount_uindex,priority:2" json:"token"`
|
Address string `gorm:"column:address;uniqueIndex:transaction_lock_network_address_token_amount_uindex,priority:2" json:"address"`
|
||||||
AmountScaled int64 `gorm:"column:amount_scaled;uniqueIndex:transaction_lock_address_token_amount_uindex,priority:3" json:"amount_scaled"`
|
Token string `gorm:"column:token;uniqueIndex:transaction_lock_network_address_token_amount_uindex,priority:3" json:"token"`
|
||||||
|
AmountScaled int64 `gorm:"column:amount_scaled;uniqueIndex:transaction_lock_network_address_token_amount_uindex,priority:4" json:"amount_scaled"`
|
||||||
AmountText string `gorm:"column:amount_text" json:"amount_text"`
|
AmountText string `gorm:"column:amount_text" json:"amount_text"`
|
||||||
TradeId string `gorm:"column:trade_id;index:transaction_lock_trade_id_index" json:"trade_id"`
|
TradeId string `gorm:"column:trade_id;index:transaction_lock_trade_id_index" json:"trade_id"`
|
||||||
ExpiresAt time.Time `gorm:"column:expires_at;index:transaction_lock_expires_at_index" json:"expires_at"`
|
ExpiresAt time.Time `gorm:"column:expires_at;index:transaction_lock_expires_at_index" json:"expires_at"`
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ const (
|
|||||||
TokenStatusDisable = 2
|
TokenStatusDisable = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NetworkTron = "tron"
|
||||||
|
NetworkSolana = "solana"
|
||||||
|
NetworkEthereum = "ethereum"
|
||||||
|
)
|
||||||
|
|
||||||
type WalletAddress struct {
|
type WalletAddress struct {
|
||||||
Network string `gorm:"column:network;uniqueIndex:wallet_address_network_address_uindex" json:"network"`
|
Network string `gorm:"column:network;uniqueIndex:wallet_address_network_address_uindex" json:"network"`
|
||||||
Address string `gorm:"column:address;uniqueIndex:wallet_address_address_uindex" json:"address"`
|
Address string `gorm:"column:address;uniqueIndex:wallet_address_network_address_uindex" json:"address"`
|
||||||
Status int64 `gorm:"column:status;default:1" json:"status"`
|
Status int64 `gorm:"column:status;default:1" json:"status"`
|
||||||
BaseModel
|
BaseModel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ type CreateTransactionRequest struct {
|
|||||||
NotifyUrl string `json:"notify_url" validate:"required"`
|
NotifyUrl string `json:"notify_url" validate:"required"`
|
||||||
Signature string `json:"signature" validate:"required"`
|
Signature string `json:"signature" validate:"required"`
|
||||||
RedirectUrl string `json:"redirect_url"`
|
RedirectUrl string `json:"redirect_url"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PaymentType string `json:"payment_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r CreateTransactionRequest) Translates() map[string]string {
|
func (r CreateTransactionRequest) Translates() map[string]string {
|
||||||
@@ -36,3 +38,18 @@ type OrderProcessingRequest struct {
|
|||||||
TradeId string
|
TradeId string
|
||||||
BlockTransactionId string
|
BlockTransactionId string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SwitchNetworkRequest 切换支付网络
|
||||||
|
type SwitchNetworkRequest struct {
|
||||||
|
TradeId string `json:"trade_id" validate:"required"`
|
||||||
|
Token string `json:"token" validate:"required"`
|
||||||
|
Network string `json:"network" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r SwitchNetworkRequest) Translates() map[string]string {
|
||||||
|
return validate.MS{
|
||||||
|
"TradeId": "订单号",
|
||||||
|
"Token": "币种",
|
||||||
|
"Network": "网络",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,3 +25,16 @@ type OrderNotifyResponse struct {
|
|||||||
Signature string `json:"signature"` // 签名
|
Signature string `json:"signature"` // 签名
|
||||||
Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OrderNotifyResponseEpay epay订单异步回调结构体
|
||||||
|
type OrderNotifyResponseEpay struct {
|
||||||
|
PID int `json:"pid"` // 商户ID
|
||||||
|
TradeNo string `json:"trade_no"` // 平台订单号
|
||||||
|
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||||
|
Type string `json:"type"` // 订单类型
|
||||||
|
Name string `json:"name"` // 商品名称
|
||||||
|
Money string `json:"money"` // 订单金额,保留4位小数
|
||||||
|
Sign string `json:"sign"` // 签名
|
||||||
|
SignType string `json:"sign_type"` // 签名类型 // MD5
|
||||||
|
TradeStatus string `json:"trade_status"` // 订单状态 // only has "TRADE_SUCCESS" for now
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type CheckoutCounterResponse struct {
|
|||||||
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||||
RedirectUrl string `json:"redirect_url"`
|
RedirectUrl string `json:"redirect_url"`
|
||||||
CreatedAt int64 `json:"created_at"` // 订单创建时间 时间戳
|
CreatedAt int64 `json:"created_at"` // 订单创建时间 时间戳
|
||||||
|
IsSelected bool `json:"is_selected"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckStatusResponse struct {
|
type CheckStatusResponse struct {
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ const (
|
|||||||
IncrementalMaximumNumber = 100
|
IncrementalMaximumNumber = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
var gCreateTransactionLock sync.Mutex
|
var (
|
||||||
var gOrderProcessingLock sync.Mutex
|
gCreateTransactionLock sync.Mutex
|
||||||
|
gOrderProcessingLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
// CreateTransaction creates a new payment order.
|
// CreateTransaction creates a new payment order.
|
||||||
func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateTransactionResponse, error) {
|
func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateTransactionResponse, error) {
|
||||||
@@ -38,6 +40,7 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
|||||||
|
|
||||||
token := strings.ToUpper(strings.TrimSpace(req.Token))
|
token := strings.ToUpper(strings.TrimSpace(req.Token))
|
||||||
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
|
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
|
||||||
|
network := strings.ToLower(strings.TrimSpace(req.Network))
|
||||||
payAmount := math.MustParsePrecFloat64(req.Amount, 2)
|
payAmount := math.MustParsePrecFloat64(req.Amount, 2)
|
||||||
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(currency))
|
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(currency))
|
||||||
if rate <= 0 {
|
if rate <= 0 {
|
||||||
@@ -61,7 +64,7 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
|||||||
return nil, constant.OrderAlreadyExists
|
return nil, constant.OrderAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
walletAddress, err := data.GetAvailableWalletAddress()
|
walletAddress, err := data.GetAvailableWalletAddressByNetwork(network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -71,7 +74,7 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
|||||||
|
|
||||||
tradeID := GenerateCode()
|
tradeID := GenerateCode()
|
||||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
||||||
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(tradeID, token, amount, walletAddress)
|
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(tradeID, network, token, amount, walletAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -88,9 +91,12 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
|||||||
ActualAmount: availableAmount,
|
ActualAmount: availableAmount,
|
||||||
ReceiveAddress: availableAddress,
|
ReceiveAddress: availableAddress,
|
||||||
Token: token,
|
Token: token,
|
||||||
|
Network: network,
|
||||||
Status: mdb.StatusWaitPay,
|
Status: mdb.StatusWaitPay,
|
||||||
NotifyUrl: req.NotifyUrl,
|
NotifyUrl: req.NotifyUrl,
|
||||||
RedirectUrl: req.RedirectUrl,
|
RedirectUrl: req.RedirectUrl,
|
||||||
|
Name: req.Name,
|
||||||
|
PaymentType: req.PaymentType,
|
||||||
}
|
}
|
||||||
if err = data.CreateOrderWithTransaction(tx, order); err != nil {
|
if err = data.CreateOrderWithTransaction(tx, order); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -148,20 +154,83 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = data.UnLockTransaction(req.ReceiveAddress, req.Token, req.Amount); err != nil {
|
if err = data.UnLockTransaction(req.Network, req.ReceiveAddress, req.Token, req.Amount); err != nil {
|
||||||
log.Sugar.Warnf("[order] unlock transaction after pay success failed, trade_id=%s, err=%v", req.TradeId, err)
|
log.Sugar.Warnf("[order] unlock transaction after pay success failed, trade_id=%s, err=%v", req.TradeId, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load order to check parent-child relationship
|
||||||
|
order, err := data.GetOrderInfoByTradeId(req.TradeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent order paid directly: expire all sub-orders and release their locks
|
||||||
|
if order.ParentTradeId == "" {
|
||||||
|
subs, subErr := data.GetActiveSubOrders(order.TradeId)
|
||||||
|
if subErr != nil {
|
||||||
|
log.Sugar.Errorf("[order] get sub-orders for parent failed, trade_id=%s, err=%v", order.TradeId, subErr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, sub := range subs {
|
||||||
|
if err = data.ExpireOrderByTradeId(sub.TradeId); err != nil {
|
||||||
|
log.Sugar.Warnf("[order] expire sub-order failed, trade_id=%s, err=%v", sub.TradeId, err)
|
||||||
|
}
|
||||||
|
if err = data.UnLockTransaction(sub.Network, sub.ReceiveAddress, sub.Token, sub.ActualAmount); err != nil {
|
||||||
|
log.Sugar.Warnf("[order] unlock sub-order transaction failed, trade_id=%s, err=%v", sub.TradeId, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub-order should not trigger its own callback (notify_url is empty).
|
||||||
|
// OrderSuccessWithTransaction unconditionally sets callback_confirm=No,
|
||||||
|
// reset it here to prevent the callback worker from retrying an empty URL.
|
||||||
|
if err = data.ResetCallbackConfirmOk(order.TradeId); err != nil {
|
||||||
|
log.Sugar.Warnf("[order] reset sub-order callback_confirm failed, trade_id=%s, err=%v", order.TradeId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := data.GetOrderInfoByTradeId(order.ParentTradeId)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[order] load parent order failed, parent_trade_id=%s, err=%v", order.ParentTradeId, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark parent as paid with sub-order's payment details
|
||||||
|
if _, err = data.MarkParentOrderSuccess(parent.TradeId, order); err != nil {
|
||||||
|
log.Sugar.Errorf("[order] mark parent success failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release parent's own wallet lock
|
||||||
|
if err = data.UnLockTransaction(parent.Network, parent.ReceiveAddress, parent.Token, parent.ActualAmount); err != nil {
|
||||||
|
log.Sugar.Warnf("[order] unlock parent transaction failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire sibling sub-orders and release their locks
|
||||||
|
siblings, err := data.GetSiblingSubOrders(parent.TradeId, order.TradeId)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[order] get sibling sub-orders failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, sib := range siblings {
|
||||||
|
if err = data.ExpireOrderByTradeId(sib.TradeId); err != nil {
|
||||||
|
log.Sugar.Warnf("[order] expire sibling failed, trade_id=%s, err=%v", sib.TradeId, err)
|
||||||
|
}
|
||||||
|
if err = data.UnLockTransaction(sib.Network, sib.ReceiveAddress, sib.Token, sib.ActualAmount); err != nil {
|
||||||
|
log.Sugar.Warnf("[order] unlock sibling transaction failed, trade_id=%s, err=%v", sib.TradeId, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReserveAvailableWalletAndAmount finds and locks an address+token+amount pair.
|
// ReserveAvailableWalletAndAmount finds and locks a network+address+token+amount pair.
|
||||||
func ReserveAvailableWalletAndAmount(tradeID string, token string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
func ReserveAvailableWalletAndAmount(tradeID string, network string, token string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
||||||
availableAddress := ""
|
availableAddress := ""
|
||||||
availableAmount := amount
|
availableAmount := amount
|
||||||
|
|
||||||
tryLockWalletFunc := func(targetAmount float64) (string, error) {
|
tryLockWalletFunc := func(targetAmount float64) (string, error) {
|
||||||
for _, address := range walletAddress {
|
for _, address := range walletAddress {
|
||||||
err := data.LockTransaction(address.Address, token, tradeID, targetAmount, config.GetOrderExpirationTimeDuration())
|
err := data.LockTransaction(network, address.Address, token, tradeID, targetAmount, config.GetOrderExpirationTimeDuration())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return address.Address, nil
|
return address.Address, nil
|
||||||
}
|
}
|
||||||
@@ -208,3 +277,141 @@ func GetOrderInfoByTradeId(tradeId string) (*mdb.Orders, error) {
|
|||||||
}
|
}
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MaxSubOrders = 2
|
||||||
|
|
||||||
|
// SwitchNetwork creates or returns an existing sub-order for a different token+network.
|
||||||
|
func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounterResponse, error) {
|
||||||
|
gCreateTransactionLock.Lock()
|
||||||
|
defer gCreateTransactionLock.Unlock()
|
||||||
|
|
||||||
|
token := strings.ToUpper(strings.TrimSpace(req.Token))
|
||||||
|
network := strings.ToLower(strings.TrimSpace(req.Network))
|
||||||
|
|
||||||
|
// 1. Load parent order
|
||||||
|
parent, err := data.GetOrderInfoByTradeId(req.TradeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if parent.ID <= 0 {
|
||||||
|
return nil, constant.OrderNotExists
|
||||||
|
}
|
||||||
|
if parent.ParentTradeId != "" {
|
||||||
|
return nil, constant.CannotSwitchSubOrder
|
||||||
|
}
|
||||||
|
if parent.Status != mdb.StatusWaitPay {
|
||||||
|
return nil, constant.OrderNotWaitPay
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Same token+network as parent → mark selected and return
|
||||||
|
if strings.EqualFold(parent.Token, token) && strings.EqualFold(parent.Network, network) {
|
||||||
|
_ = data.MarkOrderSelected(parent.TradeId)
|
||||||
|
parent.IsSelected = true
|
||||||
|
return buildCheckoutResponse(parent), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Existing active sub-order for this token+network → return it
|
||||||
|
existing, err := data.GetSubOrderByTokenNetwork(parent.TradeId, token, network)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existing.ID > 0 {
|
||||||
|
_ = data.MarkOrderSelected(parent.TradeId)
|
||||||
|
_ = data.MarkOrderSelected(existing.TradeId)
|
||||||
|
_ = data.RefreshOrderExpiration(parent.TradeId)
|
||||||
|
existing.IsSelected = true
|
||||||
|
return buildCheckoutResponse(existing), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check sub-order limit
|
||||||
|
count, err := data.CountActiveSubOrders(parent.TradeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if count >= MaxSubOrders {
|
||||||
|
return nil, constant.SubOrderLimitExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Calculate amount for the new network
|
||||||
|
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(parent.Currency))
|
||||||
|
if rate <= 0 {
|
||||||
|
return nil, constant.RateAmountErr
|
||||||
|
}
|
||||||
|
decimalPayAmount := decimal.NewFromFloat(parent.Amount)
|
||||||
|
decimalTokenAmount := decimalPayAmount.Mul(decimal.NewFromFloat(rate))
|
||||||
|
if decimalTokenAmount.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
|
||||||
|
return nil, constant.PayAmountErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Find and lock wallet
|
||||||
|
walletAddress, err := data.GetAvailableWalletAddressByNetwork(network)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(walletAddress) <= 0 {
|
||||||
|
return nil, constant.NotAvailableWalletAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
subTradeID := GenerateCode()
|
||||||
|
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
||||||
|
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(subTradeID, network, token, amount, walletAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if availableAddress == "" {
|
||||||
|
return nil, constant.NotAvailableAmountErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Create sub-order
|
||||||
|
tx := dao.Mdb.Begin()
|
||||||
|
subOrder := &mdb.Orders{
|
||||||
|
TradeId: subTradeID,
|
||||||
|
OrderId: subTradeID, // sub-order uses its own trade_id as order_id (unique constraint)
|
||||||
|
ParentTradeId: parent.TradeId,
|
||||||
|
Amount: parent.Amount,
|
||||||
|
Currency: parent.Currency,
|
||||||
|
ActualAmount: availableAmount,
|
||||||
|
ReceiveAddress: availableAddress,
|
||||||
|
Token: token,
|
||||||
|
Network: network,
|
||||||
|
Status: mdb.StatusWaitPay,
|
||||||
|
IsSelected: true,
|
||||||
|
NotifyUrl: "",
|
||||||
|
RedirectUrl: parent.RedirectUrl,
|
||||||
|
Name: parent.Name,
|
||||||
|
CallBackConfirm: mdb.CallBackConfirmOk, // don't trigger callback on sub-order
|
||||||
|
PaymentType: parent.PaymentType,
|
||||||
|
}
|
||||||
|
if err = data.CreateOrderWithTransaction(tx, subOrder); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
_ = data.UnLockTransactionByTradeId(subTradeID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = tx.Commit().Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
_ = data.UnLockTransactionByTradeId(subTradeID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark parent as selected and refresh its expiration to match the sub-order
|
||||||
|
_ = data.MarkOrderSelected(parent.TradeId)
|
||||||
|
_ = data.RefreshOrderExpiration(parent.TradeId)
|
||||||
|
|
||||||
|
return buildCheckoutResponse(subOrder), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse {
|
||||||
|
return &response.CheckoutCounterResponse{
|
||||||
|
TradeId: order.TradeId,
|
||||||
|
Amount: order.Amount,
|
||||||
|
ActualAmount: order.ActualAmount,
|
||||||
|
Token: order.Token,
|
||||||
|
Currency: order.Currency,
|
||||||
|
ReceiveAddress: order.ReceiveAddress,
|
||||||
|
Network: order.Network,
|
||||||
|
ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||||
|
RedirectUrl: order.RedirectUrl,
|
||||||
|
CreatedAt: order.CreatedAt.TimestampMilli(),
|
||||||
|
IsSelected: order.IsSelected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func newCreateTransactionRequest(orderID string, amount float64) *request.Create
|
|||||||
OrderId: orderID,
|
OrderId: orderID,
|
||||||
Currency: "CNY",
|
Currency: "CNY",
|
||||||
Token: "USDT",
|
Token: "USDT",
|
||||||
|
Network: "tron",
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
NotifyUrl: "https://merchant.example/callback",
|
NotifyUrl: "https://merchant.example/callback",
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
|
|||||||
t.Fatalf("unexpected tokens: %s, %s", resp1.Token, resp2.Token)
|
t.Fatalf("unexpected tokens: %s, %s", resp1.Token, resp2.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeID1, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp1.ReceiveAddress, resp1.Token, resp1.ActualAmount)
|
tradeID1, err := data.GetTradeIdByWalletAddressAndAmountAndToken("tron", resp1.ReceiveAddress, resp1.Token, resp1.ActualAmount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get first runtime lock: %v", err)
|
t.Fatalf("get first runtime lock: %v", err)
|
||||||
}
|
}
|
||||||
@@ -61,7 +62,7 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
|
|||||||
t.Fatalf("first runtime lock = %s, want %s", tradeID1, resp1.TradeId)
|
t.Fatalf("first runtime lock = %s, want %s", tradeID1, resp1.TradeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeID2, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp2.ReceiveAddress, resp2.Token, resp2.ActualAmount)
|
tradeID2, err := data.GetTradeIdByWalletAddressAndAmountAndToken("tron", resp2.ReceiveAddress, resp2.Token, resp2.ActualAmount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get second runtime lock: %v", err)
|
t.Fatalf("get second runtime lock: %v", err)
|
||||||
}
|
}
|
||||||
@@ -86,6 +87,7 @@ func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) {
|
|||||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||||
ReceiveAddress: resp.ReceiveAddress,
|
ReceiveAddress: resp.ReceiveAddress,
|
||||||
Token: resp.Token,
|
Token: resp.Token,
|
||||||
|
Network: "tron",
|
||||||
TradeId: resp.TradeId,
|
TradeId: resp.TradeId,
|
||||||
Amount: resp.ActualAmount,
|
Amount: resp.ActualAmount,
|
||||||
BlockTransactionId: "block_1",
|
BlockTransactionId: "block_1",
|
||||||
@@ -108,7 +110,7 @@ func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) {
|
|||||||
t.Fatalf("block transaction id = %s, want block_1", order.BlockTransactionId)
|
t.Fatalf("block transaction id = %s, want block_1", order.BlockTransactionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp.ReceiveAddress, resp.Token, resp.ActualAmount)
|
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken("tron", resp.ReceiveAddress, resp.Token, resp.ActualAmount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get runtime lock after processing: %v", err)
|
t.Fatalf("get runtime lock after processing: %v", err)
|
||||||
}
|
}
|
||||||
@@ -133,6 +135,7 @@ func TestOrderProcessingRejectsDuplicateBlockForSameOrder(t *testing.T) {
|
|||||||
req := &request.OrderProcessingRequest{
|
req := &request.OrderProcessingRequest{
|
||||||
ReceiveAddress: resp.ReceiveAddress,
|
ReceiveAddress: resp.ReceiveAddress,
|
||||||
Token: resp.Token,
|
Token: resp.Token,
|
||||||
|
Network: "tron",
|
||||||
TradeId: resp.TradeId,
|
TradeId: resp.TradeId,
|
||||||
Amount: resp.ActualAmount,
|
Amount: resp.ActualAmount,
|
||||||
BlockTransactionId: "block_1",
|
BlockTransactionId: "block_1",
|
||||||
@@ -180,6 +183,7 @@ func TestOrderProcessingDoesNotReviveExpiredOrder(t *testing.T) {
|
|||||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||||
ReceiveAddress: resp.ReceiveAddress,
|
ReceiveAddress: resp.ReceiveAddress,
|
||||||
Token: resp.Token,
|
Token: resp.Token,
|
||||||
|
Network: "tron",
|
||||||
TradeId: resp.TradeId,
|
TradeId: resp.TradeId,
|
||||||
Amount: resp.ActualAmount,
|
Amount: resp.ActualAmount,
|
||||||
BlockTransactionId: "block_expired",
|
BlockTransactionId: "block_expired",
|
||||||
@@ -239,6 +243,7 @@ func TestOrderProcessingOnlyOneOrderClaimsABlockTransaction(t *testing.T) {
|
|||||||
errs <- OrderProcessing(&request.OrderProcessingRequest{
|
errs <- OrderProcessing(&request.OrderProcessingRequest{
|
||||||
ReceiveAddress: address,
|
ReceiveAddress: address,
|
||||||
Token: token,
|
Token: token,
|
||||||
|
Network: "tron",
|
||||||
TradeId: tradeID,
|
TradeId: tradeID,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
BlockTransactionId: "shared_block",
|
BlockTransactionId: "shared_block",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterRespo
|
|||||||
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||||
RedirectUrl: orderInfo.RedirectUrl,
|
RedirectUrl: orderInfo.RedirectUrl,
|
||||||
CreatedAt: orderInfo.CreatedAt.TimestampMilli(),
|
CreatedAt: orderInfo.CreatedAt.TimestampMilli(),
|
||||||
|
IsSelected: orderInfo.IsSelected,
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+545
-169
@@ -1,22 +1,419 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"encoding/json"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/assimon/luuu/config"
|
||||||
|
"github.com/assimon/luuu/model/data"
|
||||||
|
"github.com/assimon/luuu/model/mdb"
|
||||||
|
"github.com/assimon/luuu/model/request"
|
||||||
|
"github.com/assimon/luuu/util/constant"
|
||||||
|
"github.com/assimon/luuu/util/log"
|
||||||
|
"github.com/assimon/luuu/util/math"
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/gagliardetto/solana-go/rpc"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SolCallBack(address string) {}
|
// gProcessedSignatures 已处理签名缓存,避免重复调用 getTransaction
|
||||||
|
var gProcessedSignatures sync.Map // sig -> unix timestamp
|
||||||
|
|
||||||
|
type TransferInfo struct {
|
||||||
|
Source string // Source address (for SOL) or source ATA (for SPL tokens)
|
||||||
|
Destination string // Destination address (for SOL) or destination ATA (for SPL tokens)
|
||||||
|
Mint string // Token mint (e.g. USDT mint, USDC mint, or "SOL" for native transfers)
|
||||||
|
Amount float64 // Human-readable amount (adjusted for decimals)
|
||||||
|
RawAmount uint64 // Raw amount from the transaction (before adjusting for decimals)
|
||||||
|
Decimals *int // Optional decimals from the transaction, if available
|
||||||
|
BlockTime int64 // Block time of the transfer unit is seconds since epoch
|
||||||
|
}
|
||||||
|
|
||||||
|
// SolCallBack 扫描指定钱包地址的 Solana 链上交易,匹配待支付订单并确认收款。
|
||||||
|
func SolCallBack(address string, wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
log.Sugar.Errorf("[SOL][%s] panic recovered: %v", address, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Clean up old entries from processed cache (older than 1 hour)
|
||||||
|
cleanupCutoff := time.Now().Add(-1 * time.Hour).Unix()
|
||||||
|
gProcessedSignatures.Range(func(key, value interface{}) bool {
|
||||||
|
if ts, ok := value.(int64); ok && ts < cleanupCutoff {
|
||||||
|
gProcessedSignatures.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
limit := 1000
|
||||||
|
|
||||||
|
// 查询钱包地址 + USDT ATA + USDC ATA 三个地址的签名
|
||||||
|
queryAddrs := []string{address}
|
||||||
|
if usdtAta, err := FindATAAddress(address, USDT_Mint); err == nil {
|
||||||
|
queryAddrs = append(queryAddrs, usdtAta)
|
||||||
|
} else {
|
||||||
|
log.Sugar.Errorf("[SOL][%s] failed to derive USDT ATA: %v", address, err)
|
||||||
|
}
|
||||||
|
if usdcAta, err := FindATAAddress(address, USDC_Mint); err == nil {
|
||||||
|
queryAddrs = append(queryAddrs, usdcAta)
|
||||||
|
} else {
|
||||||
|
log.Sugar.Errorf("[SOL][%s] failed to derive USDC ATA: %v", address, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉取签名并去重
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var result []solSignatureResult
|
||||||
|
for _, queryAddr := range queryAddrs {
|
||||||
|
respBody, err := SolGetSignaturesForAddress(queryAddr, limit, "", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[SOL][%s] SolGetSignaturesForAddress(%s) failed: %v", address, queryAddr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBody := gjson.GetBytes(respBody, "result")
|
||||||
|
if !resultBody.Exists() || !resultBody.IsArray() {
|
||||||
|
log.Sugar.Errorf("[SOL][%s] unexpected response format for %s: %s", address, queryAddr, string(respBody))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var batch []solSignatureResult
|
||||||
|
err = json.Unmarshal([]byte(resultBody.Raw), &batch)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[SOL][%s] failed to unmarshal signatures for %s: %v", address, queryAddr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sig := range batch {
|
||||||
|
if !seen[sig.Signature] {
|
||||||
|
seen[sig.Signature] = true
|
||||||
|
result = append(result, sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
log.Sugar.Debugf("[SOL][%s] no transaction signatures found", address)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 blockTime 降序排列
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
if result[i].BlockTime == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if result[j].BlockTime == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *result[i].BlockTime > *result[j].BlockTime
|
||||||
|
})
|
||||||
|
|
||||||
|
// 时间截止线:订单过期时间 + 5 分钟
|
||||||
|
cutoffTime := time.Now().Add(-config.GetOrderExpirationTimeDuration() - 5*time.Minute).Unix()
|
||||||
|
|
||||||
|
log.Sugar.Debugf("[SOL][%s] fetched %d unique signatures from %d addresses, cutoff=%d",
|
||||||
|
address, len(result), len(queryAddrs), cutoffTime)
|
||||||
|
|
||||||
|
// Process each transaction signature
|
||||||
|
for sigIdx, txSig := range result {
|
||||||
|
sig := txSig.Signature
|
||||||
|
|
||||||
|
// Skip failed transactions
|
||||||
|
if txSig.Err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超过截止时间的旧签名不再处理
|
||||||
|
if txSig.BlockTime != nil && *txSig.BlockTime < cutoffTime {
|
||||||
|
log.Sugar.Debugf("[SOL][%s] [%d/%d] sig=%s blockTime=%d before cutoff=%d, stopping scan",
|
||||||
|
address, sigIdx+1, len(result), sig, *txSig.BlockTime, cutoffTime)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过已处理的签名
|
||||||
|
if _, ok := gProcessedSignatures.Load(sig); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Sugar.Debugf("[SOL][%s] [%d/%d] processing sig=%s slot=%d", address, sigIdx+1, len(result), sig, txSig.Slot)
|
||||||
|
|
||||||
|
txData, err := SolGetTransaction(sig)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Debugf("[SOL][%s] sig=%s fetch failed: %v", address, sig, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
instructions := gjson.GetBytes(txData, "result.transaction.message.instructions").Array()
|
||||||
|
log.Sugar.Debugf("[SOL][%s] sig=%s has %d instructions", address, sig, len(instructions))
|
||||||
|
|
||||||
|
for instrIdx, instruction := range instructions {
|
||||||
|
programID := instruction.Get("programId").String()
|
||||||
|
parsedType := instruction.Get("parsed.type").String()
|
||||||
|
log.Sugar.Debugf("[SOL][%s] sig=%s instr[%d] programId=%s parsedType=%s", address, sig, instrIdx, programID, parsedType)
|
||||||
|
|
||||||
|
transferInfo, err := ParseTransferInfoFromInstruction(instruction, txData)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Debugf("[SOL][%s] sig=%s instr[%d] parse error: %v", address, sig, instrIdx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if transferInfo == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Sugar.Debugf("[SOL][%s] sig=%s instr[%d] transfer: src=%s dst=%s mint=%s rawAmount=%d amount=%.6f blockTime=%d",
|
||||||
|
address, sig, instrIdx,
|
||||||
|
transferInfo.Source, transferInfo.Destination, transferInfo.Mint,
|
||||||
|
transferInfo.RawAmount, transferInfo.Amount, transferInfo.BlockTime)
|
||||||
|
|
||||||
|
if !isTransferToAddress(transferInfo, address) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
token, amount := getTokenTypeAndAmount(transferInfo)
|
||||||
|
if token == "" || amount <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Sugar.Infof("[SOL][%s] sig=%s instr[%d] incoming transfer confirmed: token=%s amount=%.2f -> querying transaction_lock network=solana address=%s token=%s amount=%.2f",
|
||||||
|
address, sig, instrIdx, token, amount, address, token, amount)
|
||||||
|
|
||||||
|
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkSolana, address, token, amount)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[SOL][%s] sig=%s query transaction_lock failed: %v", address, sig, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tradeID == "" {
|
||||||
|
log.Sugar.Infof("[SOL][%s] sig=%s no active transaction_lock matched: network=solana address=%s token=%s amount=%.2f (no order or expired)",
|
||||||
|
address, sig, address, token, amount)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Sugar.Infof("[SOL][%s] transaction_lock matched: trade_id=%s sig=%s token=%s amount=%.2f",
|
||||||
|
address, tradeID, sig, token, amount)
|
||||||
|
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[SOL][%s] sig=%s load order failed for trade_id=%s: %v", address, sig, tradeID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Sugar.Infof("[SOL][%s] order loaded: trade_id=%s order_id=%s status=%d created_at_ms=%d",
|
||||||
|
address, tradeID, order.OrderId, order.Status, order.CreatedAt.TimestampMilli())
|
||||||
|
|
||||||
|
// blockTime 秒 → 毫秒,与订单创建时间对齐
|
||||||
|
blockTimestamp := transferInfo.BlockTime * 1000
|
||||||
|
createTime := order.CreatedAt.TimestampMilli()
|
||||||
|
log.Sugar.Infof("[SOL][%s] time check: sig=%s block_time_ms=%d order_created_ms=%d diff_ms=%d",
|
||||||
|
address, sig, blockTimestamp, createTime, blockTimestamp-createTime)
|
||||||
|
if blockTimestamp < createTime {
|
||||||
|
log.Sugar.Warnf("[SOL][%s] sig=%s skipped: block_time_ms=%d is %d ms before order created_ms=%d (transaction predates the order)",
|
||||||
|
address, sig, blockTimestamp, createTime-blockTimestamp, createTime)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &request.OrderProcessingRequest{
|
||||||
|
ReceiveAddress: address,
|
||||||
|
Token: token,
|
||||||
|
Network: mdb.NetworkSolana,
|
||||||
|
TradeId: tradeID,
|
||||||
|
Amount: amount,
|
||||||
|
BlockTransactionId: sig,
|
||||||
|
}
|
||||||
|
log.Sugar.Infof("[SOL][%s] calling OrderProcessing: trade_id=%s sig=%s token=%s amount=%.2f",
|
||||||
|
address, tradeID, sig, token, amount)
|
||||||
|
err = OrderProcessing(req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
|
||||||
|
log.Sugar.Infof("[SOL][%s] sig=%s already resolved: trade_id=%s reason=%v", address, sig, tradeID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Sugar.Errorf("[SOL][%s] sig=%s OrderProcessing failed for trade_id=%s: %v", address, sig, tradeID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Sugar.Infof("[SOL][%s] order marked paid: trade_id=%s sig=%s token=%s amount=%.2f, sending telegram notification",
|
||||||
|
address, tradeID, sig, token, amount)
|
||||||
|
sendPaymentNotification(order)
|
||||||
|
log.Sugar.Infof("[SOL][%s] payment fully processed: trade_id=%s sig=%s", address, tradeID, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记已处理
|
||||||
|
gProcessedSignatures.Store(sig, time.Now().Unix())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// solSignatureResult getSignaturesForAddress 返回结构
|
||||||
|
type solSignatureResult struct {
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
Slot uint64 `json:"slot"`
|
||||||
|
Err interface{} `json:"err"`
|
||||||
|
BlockTime *int64 `json:"blockTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SolRetryClient 发送 Solana JSON-RPC 请求,自动重试
|
||||||
|
func SolRetryClient(method string, params []interface{}) ([]byte, error) {
|
||||||
|
client := resty.New()
|
||||||
|
client.SetRetryCount(5)
|
||||||
|
client.SetRetryWaitTime(2 * time.Second)
|
||||||
|
client.SetRetryMaxWaitTime(10 * time.Second)
|
||||||
|
client.AddRetryCondition(func(r *resty.Response, err error) bool {
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if r.StatusCode() >= 429 || r.StatusCode() >= 500 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
rpcUrl := config.GetSolanaRpcUrl()
|
||||||
|
|
||||||
|
resp, err := client.R().
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody(map[string]interface{}{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": method,
|
||||||
|
"params": params,
|
||||||
|
}).
|
||||||
|
Post(rpcUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody := resp.Body()
|
||||||
|
|
||||||
|
return respBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SolGetSignaturesForAddress(address string, limit int, untilSig string, beforeSig string) ([]byte, error) {
|
||||||
|
opts := map[string]interface{}{
|
||||||
|
"commitment": "finalized",
|
||||||
|
"limit": limit,
|
||||||
|
}
|
||||||
|
if untilSig != "" {
|
||||||
|
opts["until"] = untilSig
|
||||||
|
}
|
||||||
|
if beforeSig != "" {
|
||||||
|
opts["before"] = beforeSig
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyData, err := SolRetryClient("getSignaturesForAddress",
|
||||||
|
[]interface{}{address, opts})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
err = json.Unmarshal(bodyData, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := result["result"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected response format: %v", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodyData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SolGetTransaction(sig string) ([]byte, error) {
|
||||||
|
txData, err := SolRetryClient("getTransaction", []interface{}{
|
||||||
|
sig,
|
||||||
|
map[string]interface{}{
|
||||||
|
"encoding": "jsonParsed",
|
||||||
|
"commitment": "confirmed",
|
||||||
|
"maxSupportedTransactionVersion": 0, // suport
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("SolRetryClient failed: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
errField := gjson.GetBytes(txData, "result.meta.err")
|
||||||
|
if errField.Exists() && errField.Type != gjson.Null {
|
||||||
|
log.Sugar.Warnf("Transaction failed: %v", errField.String())
|
||||||
|
return nil, fmt.Errorf("transaction failed: %s", errField.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return txData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTransferToAddress 判断转账目标是否为指定钱包地址
|
||||||
|
func isTransferToAddress(transfer *TransferInfo, targetAddress string) bool {
|
||||||
|
// Native SOL transfer - check destination directly
|
||||||
|
if transfer.Mint == "SOL" {
|
||||||
|
return strings.EqualFold(transfer.Destination, targetAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip Transfer instruction without mint info (use TransferChecked instead)
|
||||||
|
if transfer.Mint == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPL Token transfer - check if destination ATA matches
|
||||||
|
return MatchAtaAddress(targetAddress, transfer.Mint, transfer.Destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTokenTypeAndAmount 根据 mint 识别代币类型并计算可读金额
|
||||||
|
func getTokenTypeAndAmount(transfer *TransferInfo) (string, float64) {
|
||||||
|
mint := transfer.Mint
|
||||||
|
|
||||||
|
// Native SOL
|
||||||
|
if mint == "SOL" {
|
||||||
|
return "SOL", transfer.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPL Tokens
|
||||||
|
switch mint {
|
||||||
|
case USDT_Mint:
|
||||||
|
decimals := USDT_Decimals
|
||||||
|
if transfer.Decimals != nil {
|
||||||
|
decimals = int(*transfer.Decimals)
|
||||||
|
}
|
||||||
|
return "USDT", ADJustAmount(transfer.RawAmount, decimals)
|
||||||
|
|
||||||
|
case USDC_Mint:
|
||||||
|
decimals := USDC_Decimals
|
||||||
|
if transfer.Decimals != nil {
|
||||||
|
decimals = int(*transfer.Decimals)
|
||||||
|
}
|
||||||
|
return "USDC", ADJustAmount(transfer.RawAmount, decimals)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unsupported token
|
||||||
|
return "", 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Mint token
|
// Mint token
|
||||||
USDT_Mint = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
USDT_Mint = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
||||||
USDC_Mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
USDC_Mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
||||||
|
|
||||||
|
USDT_Decimals = 6
|
||||||
|
USDC_Decimals = 6
|
||||||
|
SOL_Decimals = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SPL Token instructions
|
||||||
|
InstructionTransfer = 3
|
||||||
|
InstructionTransferChecked = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// System Program
|
||||||
|
SystemProgramID = "11111111111111111111111111111111"
|
||||||
|
InstructionSystemTransfer = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -24,6 +421,49 @@ const (
|
|||||||
Token2022ProgramID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
Token2022ProgramID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ADJustAmount 将链上原始金额转为可读金额(除以 10^decimals,保留 2 位小数)
|
||||||
|
func ADJustAmount(amount uint64, decimals int) float64 {
|
||||||
|
if amount == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
decimalAmount := decimal.NewFromBigInt(new(big.Int).SetUint64(amount), 0)
|
||||||
|
// 10^decimals
|
||||||
|
decimalDivisor := decimal.New(1, int32(decimals))
|
||||||
|
adjustedAmount := decimalAmount.Div(decimalDivisor)
|
||||||
|
// Round to 2 decimal places
|
||||||
|
return math.MustParsePrecFloat64(adjustedAmount.InexactFloat64(), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchUsdtAtaAddress(address string, ataTo string) bool {
|
||||||
|
ata, err := FindATAAddress(address, USDT_Mint)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("FindATAAddress failed: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.EqualFold(ata, ataTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchUsdcAtaAddress(address string, ataTo string) bool {
|
||||||
|
ata, err := FindATAAddress(address, USDC_Mint)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("FindATAAddress failed: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.EqualFold(ata, ataTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchAtaAddress(address string, mint string, ataTo string) bool {
|
||||||
|
ata, err := FindATAAddress(address, mint)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("FindATAAddress failed: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.EqualFold(ata, ataTo)
|
||||||
|
}
|
||||||
|
|
||||||
func FindATAAddress(owner, mint string) (string, error) {
|
func FindATAAddress(owner, mint string) (string, error) {
|
||||||
ownerPubKey, err := solana.PublicKeyFromBase58(owner)
|
ownerPubKey, err := solana.PublicKeyFromBase58(owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -43,191 +483,127 @@ func FindATAAddress(owner, mint string) (string, error) {
|
|||||||
return ata.String(), nil
|
return ata.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// ParseTransferInfoFromInstruction 从单条指令中解析转账信息,非转账指令返回 nil
|
||||||
InstructionTransfer = 3
|
func ParseTransferInfoFromInstruction(instruction gjson.Result, txData []byte) (*TransferInfo, error) {
|
||||||
InstructionTransferChecked = 12
|
programID := instruction.Get("programId").String()
|
||||||
)
|
parsedType := instruction.Get("parsed.type").String()
|
||||||
|
|
||||||
type TransferInfo struct {
|
if programID == SystemProgramID && parsedType == "transfer" {
|
||||||
ProgramID string
|
return parseSystemTransfer(instruction, txData)
|
||||||
Type string
|
}
|
||||||
|
|
||||||
Source string
|
if programID == TokenProgramID || programID == Token2022ProgramID {
|
||||||
Destination string
|
switch parsedType {
|
||||||
Mint string
|
case "transfer":
|
||||||
Authority string
|
return parseSplTransfer(instruction, txData)
|
||||||
|
case "transferChecked":
|
||||||
|
return parseSplTransferChecked(instruction, txData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Amount uint64
|
// Skip non-transfer instructions (ComputeBudget, AToken create, etc.)
|
||||||
Decimals *uint8
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isTokenProgram(programID solana.PublicKey) bool {
|
func parseSystemTransfer(instruction gjson.Result, txData []byte) (*TransferInfo, error) {
|
||||||
p := programID.String()
|
info := instruction.Get("parsed.info")
|
||||||
return p == TokenProgramID || p == Token2022ProgramID
|
source := info.Get("source").String()
|
||||||
|
destination := info.Get("destination").String()
|
||||||
|
lamports := info.Get("lamports").Uint()
|
||||||
|
blockTime := gjson.GetBytes(txData, "result.blockTime").Int()
|
||||||
|
|
||||||
|
return &TransferInfo{
|
||||||
|
Source: source,
|
||||||
|
Destination: destination,
|
||||||
|
Mint: "SOL",
|
||||||
|
Amount: ADJustAmount(lamports, SOL_Decimals),
|
||||||
|
RawAmount: lamports,
|
||||||
|
BlockTime: blockTime,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeTransfer(data []byte, accountKeys []solana.PublicKey, accountIndexes []uint16, programID solana.PublicKey) (*TransferInfo, error) {
|
// parseSplTransfer 解析 SPL Token "transfer" 指令,mint 从 postTokenBalances 中查找
|
||||||
if len(data) < 9 {
|
func parseSplTransfer(instruction gjson.Result, txData []byte) (*TransferInfo, error) {
|
||||||
return nil, fmt.Errorf("invalid transfer data length: %d", len(data))
|
info := instruction.Get("parsed.info")
|
||||||
|
source := info.Get("source").String()
|
||||||
|
destination := info.Get("destination").String()
|
||||||
|
amountStr := info.Get("amount").String()
|
||||||
|
blockTime := gjson.GetBytes(txData, "result.blockTime").Int()
|
||||||
|
|
||||||
|
rawAmount, ok := new(big.Int).SetString(amountStr, 10)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid amount: %s", amountStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if data[0] != InstructionTransfer {
|
// Look up mint and decimals from postTokenBalances using the destination ATA
|
||||||
return nil, fmt.Errorf("not transfer instruction, got: %d", data[0])
|
mint, decimals, found := findMintFromTokenBalances(destination, txData)
|
||||||
|
if !found {
|
||||||
|
// Try source ATA as fallback
|
||||||
|
mint, decimals, found = findMintFromTokenBalances(source, txData)
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("could not determine mint for transfer: source=%s dest=%s", source, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(accountIndexes) < 3 {
|
d := decimals
|
||||||
return nil, fmt.Errorf("transfer accounts not enough: %d", len(accountIndexes))
|
return &TransferInfo{
|
||||||
}
|
Source: source,
|
||||||
|
Destination: destination,
|
||||||
amount := binary.LittleEndian.Uint64(data[1:9])
|
Mint: mint,
|
||||||
|
Amount: ADJustAmount(rawAmount.Uint64(), decimals),
|
||||||
sourceIdx := accountIndexes[0]
|
RawAmount: rawAmount.Uint64(),
|
||||||
destIdx := accountIndexes[1]
|
Decimals: &d,
|
||||||
authIdx := accountIndexes[2]
|
BlockTime: blockTime,
|
||||||
|
}, nil
|
||||||
if int(sourceIdx) >= len(accountKeys) || int(destIdx) >= len(accountKeys) || int(authIdx) >= len(accountKeys) {
|
|
||||||
return nil, errors.New("account index out of range")
|
|
||||||
}
|
|
||||||
|
|
||||||
info := &TransferInfo{
|
|
||||||
ProgramID: programID.String(),
|
|
||||||
Type: "transfer",
|
|
||||||
Source: accountKeys[sourceIdx].String(),
|
|
||||||
Destination: accountKeys[destIdx].String(),
|
|
||||||
Authority: accountKeys[authIdx].String(),
|
|
||||||
Amount: amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
return info, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferCheckedInfo struct {
|
// parseSplTransferChecked 解析 SPL Token "transferChecked" 指令
|
||||||
ProgramID string
|
func parseSplTransferChecked(instruction gjson.Result, txData []byte) (*TransferInfo, error) {
|
||||||
Type string
|
info := instruction.Get("parsed.info")
|
||||||
|
source := info.Get("source").String()
|
||||||
|
destination := info.Get("destination").String()
|
||||||
|
mint := info.Get("mint").String()
|
||||||
|
amountStr := info.Get("tokenAmount.amount").String()
|
||||||
|
decimals := int(info.Get("tokenAmount.decimals").Int())
|
||||||
|
blockTime := gjson.GetBytes(txData, "result.blockTime").Int()
|
||||||
|
|
||||||
Source string
|
rawAmount, ok := new(big.Int).SetString(amountStr, 10)
|
||||||
Destination string
|
if !ok {
|
||||||
Mint string
|
return nil, fmt.Errorf("invalid amount: %s", amountStr)
|
||||||
Authority string
|
|
||||||
|
|
||||||
Amount uint64
|
|
||||||
Decimals uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeTransferCheck(data []byte, accountKeys []solana.PublicKey, accountIndexes []uint16, programID solana.PublicKey) (*TransferInfo, error) {
|
|
||||||
if len(data) < 10 {
|
|
||||||
return nil, fmt.Errorf("invalid transferChecked data length: %d", len(data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if data[0] != InstructionTransferChecked {
|
return &TransferInfo{
|
||||||
return nil, fmt.Errorf("not transferChecked instruction, got: %d", data[0])
|
Source: source,
|
||||||
}
|
Destination: destination,
|
||||||
|
Mint: mint,
|
||||||
if len(accountIndexes) < 4 {
|
Amount: ADJustAmount(rawAmount.Uint64(), decimals),
|
||||||
return nil, fmt.Errorf("transferChecked accounts not enough: %d", len(accountIndexes))
|
RawAmount: rawAmount.Uint64(),
|
||||||
}
|
|
||||||
|
|
||||||
amount := binary.LittleEndian.Uint64(data[1:9])
|
|
||||||
decimals := uint8(data[9])
|
|
||||||
|
|
||||||
sourceIdx := accountIndexes[0]
|
|
||||||
mintIdx := accountIndexes[1]
|
|
||||||
destIdx := accountIndexes[2]
|
|
||||||
authIdx := accountIndexes[3]
|
|
||||||
|
|
||||||
if int(sourceIdx) >= len(accountKeys) ||
|
|
||||||
int(mintIdx) >= len(accountKeys) ||
|
|
||||||
int(destIdx) >= len(accountKeys) ||
|
|
||||||
int(authIdx) >= len(accountKeys) {
|
|
||||||
return nil, errors.New("account index out of range")
|
|
||||||
}
|
|
||||||
|
|
||||||
info := &TransferInfo{
|
|
||||||
ProgramID: programID.String(),
|
|
||||||
Type: "transferChecked",
|
|
||||||
Source: accountKeys[sourceIdx].String(),
|
|
||||||
Mint: accountKeys[mintIdx].String(),
|
|
||||||
Destination: accountKeys[destIdx].String(),
|
|
||||||
Authority: accountKeys[authIdx].String(),
|
|
||||||
Amount: amount,
|
|
||||||
Decimals: &decimals,
|
Decimals: &decimals,
|
||||||
}
|
BlockTime: blockTime,
|
||||||
|
}, nil
|
||||||
return info, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseTransactionTransfers
|
// findMintFromTokenBalances 从 postTokenBalances 中查找 ATA 对应的 mint 和 decimals
|
||||||
func ParseTransactionTransfers(ctx context.Context, client *rpc.Client, sig string) ([]*TransferInfo, error) {
|
func findMintFromTokenBalances(ataAddress string, txData []byte) (string, int, bool) {
|
||||||
signature, err := solana.SignatureFromBase58(sig)
|
accountKeys := gjson.GetBytes(txData, "result.transaction.message.accountKeys").Array()
|
||||||
if err != nil {
|
accountIndex := -1
|
||||||
return nil, fmt.Errorf("invalid signature: %w", err)
|
for i, key := range accountKeys {
|
||||||
|
if key.Get("pubkey").String() == ataAddress {
|
||||||
|
accountIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if accountIndex == -1 {
|
||||||
|
return "", 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := client.GetTransaction(
|
balances := gjson.GetBytes(txData, "result.meta.postTokenBalances").Array()
|
||||||
ctx,
|
for _, balance := range balances {
|
||||||
signature,
|
if int(balance.Get("accountIndex").Int()) == accountIndex {
|
||||||
&rpc.GetTransactionOpts{
|
mint := balance.Get("mint").String()
|
||||||
Encoding: solana.EncodingBase64,
|
decimals := int(balance.Get("uiTokenAmount.decimals").Int())
|
||||||
Commitment: rpc.CommitmentConfirmed,
|
return mint, decimals, true
|
||||||
MaxSupportedTransactionVersion: func(v uint64) *uint64 { return &v }(0),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get transaction failed: %w", err)
|
|
||||||
}
|
|
||||||
if tx == nil {
|
|
||||||
return nil, errors.New("transaction not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
decodedTx, err := tx.Transaction.GetTransaction()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("decode transaction failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := decodedTx.Message
|
|
||||||
accountKeys := msg.AccountKeys
|
|
||||||
results := make([]*TransferInfo, 0)
|
|
||||||
|
|
||||||
for _, ix := range msg.Instructions {
|
|
||||||
if int(ix.ProgramIDIndex) >= len(accountKeys) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
programID := accountKeys[ix.ProgramIDIndex]
|
|
||||||
if !isTokenProgram(programID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data := ix.Data
|
|
||||||
if len(data) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch data[0] {
|
|
||||||
case InstructionTransfer:
|
|
||||||
info, err := DecodeTransfer(data, accountKeys, toUint16Slice(ix.Accounts), programID)
|
|
||||||
if err == nil {
|
|
||||||
results = append(results, info)
|
|
||||||
}
|
|
||||||
case InstructionTransferChecked:
|
|
||||||
info, err := DecodeTransferCheck(data, accountKeys, toUint16Slice(ix.Accounts), programID)
|
|
||||||
if err == nil {
|
|
||||||
results = append(results, info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return "", 0, false
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUint16Slice(in []uint16) []uint16 {
|
|
||||||
out := make([]uint16, len(in))
|
|
||||||
for i, v := range in {
|
|
||||||
out[i] = uint16(v)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeBase64InstructionData(s string) ([]byte, error) {
|
|
||||||
return base64.StdEncoding.DecodeString(s)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,349 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gagliardetto/solana-go/rpc"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseSolTransaction(t *testing.T) {
|
func TestSolClientHealthy(t *testing.T) {
|
||||||
client := rpc.New("https://api.mainnet-beta.solana.com")
|
bodyData, err := SolRetryClient("getHealth", nil)
|
||||||
sig := ""
|
|
||||||
|
|
||||||
txInfo, err := ParseTransactionTransfers(context.Background(), client, sig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
t.Fatalf("SolRetryClient failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range txInfo {
|
var result map[string]interface{}
|
||||||
fmt.Println("type =", item.Type)
|
err = json.Unmarshal(bodyData, &result)
|
||||||
fmt.Println("program =", item.ProgramID)
|
if err != nil {
|
||||||
fmt.Println("from =", item.Source)
|
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||||
fmt.Println("to =", item.Destination)
|
|
||||||
fmt.Println("mint =", item.Mint)
|
|
||||||
fmt.Println("authority =", item.Authority)
|
|
||||||
fmt.Println("amount =", item.Amount)
|
|
||||||
if item.Decimals != nil {
|
|
||||||
fmt.Println("decimals =", *item.Decimals)
|
|
||||||
}
|
}
|
||||||
fmt.Println("-----")
|
|
||||||
|
status, ok := result["result"].(string)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Unexpected response format: %v", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Logf("RPC Health Status: %s", status)
|
||||||
|
|
||||||
|
if status != "ok" {
|
||||||
|
t.Errorf("Expected health status 'ok', got '%s'", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSolClientGetSignaturesForAddress(t *testing.T) {
|
||||||
|
// Example wallet address (replace with actual test address)
|
||||||
|
address := "2uFTf9TZ8gd7Kg6hkb79TxfaeNpaAgpJ8uVHguv2Yweu"
|
||||||
|
|
||||||
|
bodyData, err := SolRetryClient("getSignaturesForAddress", []interface{}{address, map[string]interface{}{"commitment": "finalized", "limit": 100}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SolRetryClient failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
err = json.Unmarshal(bodyData, &result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signatures, ok := result["result"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Unexpected response format: %v", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Found %d signatures for address %s", len(signatures), address)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSolClientGetTransaction(t *testing.T) {
|
||||||
|
// Example transaction signature (replace with actual test signature)
|
||||||
|
sig := "2aEoNykk4ZJ27C3y7EDJiQUc7GFnnsMe7ofFzB73swGL8kTxSBFCnwzWw3jzr3BND7k8hx15fZHUUAbG1XemNFe5"
|
||||||
|
|
||||||
|
txData, err := SolRetryClient("getTransaction", []interface{}{sig, map[string]interface{}{"encoding": "jsonParsed", "commitment": "finalized"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SolRetryClient failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", string(txData))
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
err = json.Unmarshal(txData, &result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
txInfo, ok := result["result"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Unexpected response format: %v", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Transaction Info for signature %s: %v", sig, txInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindATAAddress(t *testing.T) {
|
func TestFindATAAddress(t *testing.T) {
|
||||||
owner := ""
|
tests := []struct {
|
||||||
mint := "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R" // ray token
|
name string
|
||||||
|
owner string
|
||||||
ata, err := FindATAAddress(owner, mint)
|
mint string
|
||||||
if err != nil {
|
want string
|
||||||
panic(err)
|
}{
|
||||||
|
{
|
||||||
|
name: "RAY token ATA",
|
||||||
|
owner: "2uFTf9TZ8gd7Kg6hkb79TxfaeNpaAgpJ8uVHguv2Yweu",
|
||||||
|
mint: "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",
|
||||||
|
want: "GgmJrwuP946uV8qAwsnXxzYrJqEwW6eGnsVnQZFS5rp4",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("ATA =", ata)
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ata, err := FindATAAddress(tt.owner, tt.mint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindATAAddress failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Owner: %s", tt.owner)
|
||||||
|
t.Logf("Mint: %s", tt.mint)
|
||||||
|
t.Logf("ATA: %s", ata)
|
||||||
|
|
||||||
|
if tt.want != "" && ata != tt.want {
|
||||||
|
t.Errorf("Expected ATA %s, got %s", tt.want, ata)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchATAAddress(t *testing.T) {
|
||||||
|
owner := "2uFTf9TZ8gd7Kg6hkb79TxfaeNpaAgpJ8uVHguv2Yweu"
|
||||||
|
mint := "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R" // ray token
|
||||||
|
expectedATA := "GgmJrwuP946uV8qAwsnXxzYrJqEwW6eGnsVnQZFS5rp4"
|
||||||
|
|
||||||
|
ok := MatchAtaAddress(owner, mint, expectedATA)
|
||||||
|
t.Logf("Owner: %s", owner)
|
||||||
|
t.Logf("Mint: %s", mint)
|
||||||
|
t.Logf("Expected ATA: %s", expectedATA)
|
||||||
|
t.Logf("Match result: %v", ok)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected ATA to match, but it didn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchUsdtAtaAddress(t *testing.T) {
|
||||||
|
// Example wallet address (replace with actual test address)
|
||||||
|
owner := "2uFTf9TZ8gd7Kg6hkb79TxfaeNpaAgpJ8uVHguv2Yweu"
|
||||||
|
|
||||||
|
ata, err := FindATAAddress(owner, USDT_Mint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindATAAddress failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Owner: %s", owner)
|
||||||
|
t.Logf("USDT Mint: %s", USDT_Mint)
|
||||||
|
t.Logf("USDT ATA: %s", ata)
|
||||||
|
|
||||||
|
ok := MatchUsdtAtaAddress(owner, ata)
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected USDT ATA to match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchUsdcAtaAddress(t *testing.T) {
|
||||||
|
// Example wallet address (replace with actual test address)
|
||||||
|
owner := "2uFTf9TZ8gd7Kg6hkb79TxfaeNpaAgpJ8uVHguv2Yweu"
|
||||||
|
|
||||||
|
ata, err := FindATAAddress(owner, USDC_Mint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindATAAddress failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Owner: %s", owner)
|
||||||
|
t.Logf("USDC Mint: %s", USDC_Mint)
|
||||||
|
t.Logf("USDC ATA: %s", ata)
|
||||||
|
|
||||||
|
ok := MatchUsdcAtaAddress(owner, ata)
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected USDC ATA to match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdjustAmount(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
amount uint64
|
||||||
|
decimals int
|
||||||
|
want float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "USDT amount (6 decimals)",
|
||||||
|
amount: 123456789,
|
||||||
|
decimals: 6,
|
||||||
|
want: 123.46,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "USDC amount (6 decimals)",
|
||||||
|
amount: 1000000,
|
||||||
|
decimals: 6,
|
||||||
|
want: 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SOL amount (9 decimals)",
|
||||||
|
amount: 1000000000,
|
||||||
|
decimals: 9,
|
||||||
|
want: 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zero amount",
|
||||||
|
amount: 0,
|
||||||
|
decimals: 6,
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Small amount",
|
||||||
|
amount: 1,
|
||||||
|
decimals: 6,
|
||||||
|
want: 0.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
adjusted := ADJustAmount(tt.amount, tt.decimals)
|
||||||
|
t.Logf("Raw amount: %d, Decimals: %d, Adjusted: %.2f", tt.amount, tt.decimals, adjusted)
|
||||||
|
|
||||||
|
if adjusted != tt.want {
|
||||||
|
t.Errorf("Expected %.2f, got %.2f", tt.want, adjusted)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTransferInfoFromInstruction_SplTransfer(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPL Token "transfer" (no mint in instruction, must look up from postTokenBalances)
|
||||||
|
sig := "3tZTwLrvmiZ59h4UzyMHPd7DPux7t9eXZgkUvEfquaoSuERrPSRNzWuSHKQM2fbiCWFDGNqoLpu2kLZnfoegVpqN"
|
||||||
|
txData, err := SolGetTransaction(sig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SolGetTransaction failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
instructions := gjson.GetBytes(txData, "result.transaction.message.instructions").Array()
|
||||||
|
var found bool
|
||||||
|
for _, inst := range instructions {
|
||||||
|
info, err := ParseTransferInfoFromInstruction(inst, txData)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("parse error (ok to skip): %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
t.Logf("SPL transfer: source=%s dest=%s mint=%s amount=%.6f raw=%d blockTime=%d",
|
||||||
|
info.Source, info.Destination, info.Mint, info.Amount, info.RawAmount, info.BlockTime)
|
||||||
|
|
||||||
|
if info.Mint == "" {
|
||||||
|
t.Error("Expected mint to be resolved from postTokenBalances")
|
||||||
|
}
|
||||||
|
if info.RawAmount != 50000 {
|
||||||
|
t.Errorf("Expected raw amount 50000, got %d", info.RawAmount)
|
||||||
|
}
|
||||||
|
if info.BlockTime == 0 {
|
||||||
|
t.Error("Expected non-zero blockTime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("No transfer instruction found in transaction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTransferInfoFromInstruction_TransferChecked(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPL Token "transferChecked" (has mint and tokenAmount in instruction)
|
||||||
|
sig := "2aEoNykk4ZJ27C3y7EDJiQUc7GFnnsMe7ofFzB73swGL8kTxSBFCnwzWw3jzr3BND7k8hx15fZHUUAbG1XemNFe5"
|
||||||
|
txData, err := SolGetTransaction(sig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SolGetTransaction failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
instructions := gjson.GetBytes(txData, "result.transaction.message.instructions").Array()
|
||||||
|
var found bool
|
||||||
|
for _, inst := range instructions {
|
||||||
|
info, err := ParseTransferInfoFromInstruction(inst, txData)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("parse error (ok to skip): %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
t.Logf("TransferChecked: source=%s dest=%s mint=%s amount=%.6f raw=%d blockTime=%d",
|
||||||
|
info.Source, info.Destination, info.Mint, info.Amount, info.RawAmount, info.BlockTime)
|
||||||
|
|
||||||
|
if info.Mint != USDT_Mint {
|
||||||
|
t.Errorf("Expected USDT mint %s, got %s", USDT_Mint, info.Mint)
|
||||||
|
}
|
||||||
|
if info.RawAmount != 300000 {
|
||||||
|
t.Errorf("Expected raw amount 300000, got %d", info.RawAmount)
|
||||||
|
}
|
||||||
|
if info.Amount != 0.3 {
|
||||||
|
t.Errorf("Expected amount 0.3, got %f", info.Amount)
|
||||||
|
}
|
||||||
|
if info.BlockTime == 0 {
|
||||||
|
t.Error("Expected non-zero blockTime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("No transferChecked instruction found in transaction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTransferInfoFromInstruction_SystemTransfer(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// System program SOL transfer
|
||||||
|
sig := "5pNMonUBvLVpxXTmyd5CGVBs49W6781g2ACnrCXhbmtz58KENYA7HSqu6hQkQweg3qQboRd8WAscphNAtiq9UtZZ"
|
||||||
|
txData, err := SolGetTransaction(sig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SolGetTransaction failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
instructions := gjson.GetBytes(txData, "result.transaction.message.instructions").Array()
|
||||||
|
transferCount := 0
|
||||||
|
for _, inst := range instructions {
|
||||||
|
info, err := ParseTransferInfoFromInstruction(inst, txData)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("parse error (ok to skip): %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
transferCount++
|
||||||
|
t.Logf("System transfer #%d: source=%s dest=%s mint=%s amount=%.9f raw=%d blockTime=%d",
|
||||||
|
transferCount, info.Source, info.Destination, info.Mint, info.Amount, info.RawAmount, info.BlockTime)
|
||||||
|
|
||||||
|
if info.Mint != "SOL" {
|
||||||
|
t.Errorf("Expected mint SOL, got %s", info.Mint)
|
||||||
|
}
|
||||||
|
if info.RawAmount == 0 {
|
||||||
|
t.Error("Expected non-zero raw amount")
|
||||||
|
}
|
||||||
|
if info.BlockTime == 0 {
|
||||||
|
t.Error("Expected non-zero blockTime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if transferCount == 0 {
|
||||||
|
t.Error("No system transfer instruction found")
|
||||||
|
}
|
||||||
|
t.Logf("Found %d system transfers", transferCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -19,6 +20,7 @@ import (
|
|||||||
"github.com/assimon/luuu/util/log"
|
"github.com/assimon/luuu/util/log"
|
||||||
"github.com/assimon/luuu/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/gookit/goutil/stdutil"
|
"github.com/gookit/goutil/stdutil"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -110,7 +112,7 @@ func checkTrxTransfers(address string, wg *sync.WaitGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
txID := transfer.Get("txID").String()
|
txID := transfer.Get("txID").String()
|
||||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(address, "TRX", amount)
|
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, address, "TRX", amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -134,6 +136,7 @@ func checkTrxTransfers(address string, wg *sync.WaitGroup) {
|
|||||||
req := &request.OrderProcessingRequest{
|
req := &request.OrderProcessingRequest{
|
||||||
ReceiveAddress: address,
|
ReceiveAddress: address,
|
||||||
Token: "TRX",
|
Token: "TRX",
|
||||||
|
Network: mdb.NetworkTron,
|
||||||
TradeId: tradeID,
|
TradeId: tradeID,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
BlockTransactionId: txID,
|
BlockTransactionId: txID,
|
||||||
@@ -212,7 +215,7 @@ func checkTrc20Transfers(address string, wg *sync.WaitGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
txID := transfer.Get("transaction_id").String()
|
txID := transfer.Get("transaction_id").String()
|
||||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(address, "USDT", amount)
|
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, address, "USDT", amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -236,6 +239,7 @@ func checkTrc20Transfers(address string, wg *sync.WaitGroup) {
|
|||||||
req := &request.OrderProcessingRequest{
|
req := &request.OrderProcessingRequest{
|
||||||
ReceiveAddress: address,
|
ReceiveAddress: address,
|
||||||
Token: "USDT",
|
Token: "USDT",
|
||||||
|
Network: mdb.NetworkTron,
|
||||||
TradeId: tradeID,
|
TradeId: tradeID,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
BlockTransactionId: txID,
|
BlockTransactionId: txID,
|
||||||
@@ -254,6 +258,84 @@ func checkTrc20Transfers(address string, wg *sync.WaitGroup) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TryProcessEthereumERC20Transfer(contract common.Address, toAddr common.Address, rawValue *big.Int, txHash string, blockTsMs int64) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
log.Sugar.Errorf("[ETH-WS] TryProcessEthereumERC20Transfer panic: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
usdt := common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")
|
||||||
|
usdc := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
|
||||||
|
var tokenSym string
|
||||||
|
switch contract {
|
||||||
|
case usdt:
|
||||||
|
tokenSym = "USDT"
|
||||||
|
case usdc:
|
||||||
|
tokenSym = "USDC"
|
||||||
|
default:
|
||||||
|
log.Sugar.Warnf("[ETH-WS] skip unsupported contract %s", contract.Hex())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
walletAddr := strings.ToLower(toAddr.Hex())
|
||||||
|
if rawValue == nil || rawValue.Sign() <= 0 {
|
||||||
|
log.Sugar.Infof("[ETH-%s][%s] skip non-positive or nil amount", tokenSym, walletAddr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decimalQuant := decimal.NewFromBigInt(rawValue, 0)
|
||||||
|
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
|
||||||
|
if amount <= 0 {
|
||||||
|
log.Sugar.Warnf("[ETH-%s][%s] skip non-positive amount %.2f", tokenSym, walletAddr, amount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkEthereum, walletAddr, tokenSym, amount)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[ETH-%s][%s] lock lookup: %v", tokenSym, walletAddr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tradeID == "" {
|
||||||
|
log.Sugar.Warnf("[ETH-%s][%s] skip unmatched tx hash=%s amount=%.2f", tokenSym, walletAddr, txHash, amount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[ETH-%s][%s] load order: %v", tokenSym, walletAddr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.ToLower(strings.TrimSpace(order.Network)) != mdb.NetworkEthereum {
|
||||||
|
log.Sugar.Warnf("[ETH-%s][%s] skip trade_id=%s network=%q", tokenSym, walletAddr, tradeID, order.Network)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.ToUpper(strings.TrimSpace(order.Token)) != tokenSym {
|
||||||
|
log.Sugar.Warnf("[ETH-%s][%s] skip trade_id=%s token mismatch order=%s", tokenSym, walletAddr, tradeID, order.Token)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &request.OrderProcessingRequest{
|
||||||
|
ReceiveAddress: walletAddr,
|
||||||
|
Token: tokenSym,
|
||||||
|
Network: mdb.NetworkEthereum,
|
||||||
|
TradeId: tradeID,
|
||||||
|
Amount: amount,
|
||||||
|
BlockTransactionId: txHash,
|
||||||
|
}
|
||||||
|
err = OrderProcessing(req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
|
||||||
|
log.Sugar.Infof("[ETH-%s][%s] skip resolved trade_id=%s hash=%s err=%v", tokenSym, walletAddr, tradeID, txHash, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Sugar.Errorf("[ETH-%s][%s] OrderProcessing: %v", tokenSym, walletAddr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPaymentNotification(order)
|
||||||
|
log.Sugar.Infof("[ETH-%s][%s] payment processed trade_id=%s hash=%s", tokenSym, walletAddr, tradeID, txHash)
|
||||||
|
}
|
||||||
|
|
||||||
func sendPaymentNotification(order *mdb.Orders) {
|
func sendPaymentNotification(order *mdb.Orders) {
|
||||||
msg := fmt.Sprintf(
|
msg := fmt.Sprintf(
|
||||||
"🎉 <b>收款成功通知</b>\n\n"+
|
"🎉 <b>收款成功通知</b>\n\n"+
|
||||||
@@ -263,6 +345,7 @@ func sendPaymentNotification(order *mdb.Orders) {
|
|||||||
"📋 <b>订单信息</b>\n"+
|
"📋 <b>订单信息</b>\n"+
|
||||||
"├ 交易号:<code>%s</code>\n"+
|
"├ 交易号:<code>%s</code>\n"+
|
||||||
"├ 订单号:<code>%s</code>\n"+
|
"├ 订单号:<code>%s</code>\n"+
|
||||||
|
"├ 网络:<code>%s</code>\n"+
|
||||||
"└ 钱包地址:<code>%s</code>\n\n"+
|
"└ 钱包地址:<code>%s</code>\n\n"+
|
||||||
"⏰ <b>时间信息</b>\n"+
|
"⏰ <b>时间信息</b>\n"+
|
||||||
"├ 创建时间:%s\n"+
|
"├ 创建时间:%s\n"+
|
||||||
@@ -273,9 +356,26 @@ func sendPaymentNotification(order *mdb.Orders) {
|
|||||||
strings.ToUpper(order.Token),
|
strings.ToUpper(order.Token),
|
||||||
order.TradeId,
|
order.TradeId,
|
||||||
order.OrderId,
|
order.OrderId,
|
||||||
|
networkDisplay(order.Network),
|
||||||
order.ReceiveAddress,
|
order.ReceiveAddress,
|
||||||
order.CreatedAt.ToDateTimeString(),
|
order.CreatedAt.ToDateTimeString(),
|
||||||
carbon.Now().ToDateTimeString(),
|
carbon.Now().ToDateTimeString(),
|
||||||
)
|
)
|
||||||
telegram.SendToBot(msg)
|
telegram.SendToBot(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func networkDisplay(n string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(n)) {
|
||||||
|
case mdb.NetworkTron:
|
||||||
|
return "Tron"
|
||||||
|
case mdb.NetworkSolana:
|
||||||
|
return "Solana"
|
||||||
|
case mdb.NetworkEthereum:
|
||||||
|
return "Ethereum"
|
||||||
|
default:
|
||||||
|
if n == "" {
|
||||||
|
return "Tron"
|
||||||
|
}
|
||||||
|
return strings.ToUpper(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+56
-2
@@ -2,7 +2,10 @@ package mq
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,6 +26,7 @@ const sqliteBusyRetryAttempts = 3
|
|||||||
type expirableOrder struct {
|
type expirableOrder struct {
|
||||||
ID uint64 `gorm:"column:id"`
|
ID uint64 `gorm:"column:id"`
|
||||||
TradeId string `gorm:"column:trade_id"`
|
TradeId string `gorm:"column:trade_id"`
|
||||||
|
Network string `gorm:"column:network"`
|
||||||
ReceiveAddress string `gorm:"column:receive_address"`
|
ReceiveAddress string `gorm:"column:receive_address"`
|
||||||
Token string `gorm:"column:token"`
|
Token string `gorm:"column:token"`
|
||||||
ActualAmount float64 `gorm:"column:actual_amount"`
|
ActualAmount float64 `gorm:"column:actual_amount"`
|
||||||
@@ -65,7 +69,7 @@ func processExpiredOrders() {
|
|||||||
var orders []expirableOrder
|
var orders []expirableOrder
|
||||||
err := withSQLiteBusyRetry(func() error {
|
err := withSQLiteBusyRetry(func() error {
|
||||||
return dao.Mdb.Model(&mdb.Orders{}).
|
return dao.Mdb.Model(&mdb.Orders{}).
|
||||||
Select("id", "trade_id", "receive_address", "token", "actual_amount").
|
Select("id", "trade_id", "network", "receive_address", "token", "actual_amount").
|
||||||
Where("status = ?", mdb.StatusWaitPay).
|
Where("status = ?", mdb.StatusWaitPay).
|
||||||
Where("created_at <= ?", expirationCutoff).
|
Where("created_at <= ?", expirationCutoff).
|
||||||
Order("id asc").
|
Order("id asc").
|
||||||
@@ -89,7 +93,7 @@ func processExpiredOrders() {
|
|||||||
if !expired {
|
if !expired {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err = data.UnLockTransaction(order.ReceiveAddress, order.Token, order.ActualAmount); err != nil {
|
if err = data.UnLockTransaction(order.Network, order.ReceiveAddress, order.Token, order.ActualAmount); err != nil {
|
||||||
log.Sugar.Warnf("[mq] release expired transaction lock failed, trade_id=%s, err=%v", order.TradeId, err)
|
log.Sugar.Warnf("[mq] release expired transaction lock failed, trade_id=%s, err=%v", order.TradeId, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,6 +165,54 @@ func processCallback(tradeID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendOrderCallback(order *mdb.Orders) error {
|
func sendOrderCallback(order *mdb.Orders) error {
|
||||||
|
|
||||||
|
switch order.PaymentType {
|
||||||
|
case mdb.PaymentTypeEpay:
|
||||||
|
// 构造 EPay 标准回调参数
|
||||||
|
notifyData := response.OrderNotifyResponseEpay{
|
||||||
|
PID: config.GetEpayPid(),
|
||||||
|
TradeNo: order.TradeId, // epusdt 订单号作为 EPay 平台订单号
|
||||||
|
OutTradeNo: order.OrderId, // 注意:EPay 回调要求商户订单号使用 out_trade_no 参数
|
||||||
|
|
||||||
|
Type: "alipay",
|
||||||
|
Name: order.Name,
|
||||||
|
Money: fmt.Sprintf("%.4f", order.Amount),
|
||||||
|
TradeStatus: "TRADE_SUCCESS",
|
||||||
|
}
|
||||||
|
|
||||||
|
signstr2, err := sign.Get(notifyData, config.GetEpayKey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 form-encoded POST(EPay 标准协议格式)
|
||||||
|
formData := url.Values{
|
||||||
|
"pid": {fmt.Sprintf("%d", notifyData.PID)},
|
||||||
|
"trade_no": {notifyData.TradeNo},
|
||||||
|
"out_trade_no": {notifyData.OutTradeNo},
|
||||||
|
"type": {notifyData.Type},
|
||||||
|
"name": {notifyData.Name},
|
||||||
|
"money": {notifyData.Money},
|
||||||
|
"trade_status": {notifyData.TradeStatus},
|
||||||
|
"sign": {signstr2},
|
||||||
|
"sign_type": {"MD5"},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.PostForm(order.NotifyUrl, formData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("notify_url response status: %d, body: %s\n", resp.StatusCode, string(responseBody))
|
||||||
|
|
||||||
|
default:
|
||||||
|
|
||||||
client := http_client.GetHttpClient()
|
client := http_client.GetHttpClient()
|
||||||
orderResp := response.OrderNotifyResponse{
|
orderResp := response.OrderNotifyResponse{
|
||||||
TradeId: order.TradeId,
|
TradeId: order.TradeId,
|
||||||
@@ -191,6 +243,8 @@ func sendOrderCallback(order *mdb.Orders) error {
|
|||||||
if string(resp.Body()) != "ok" {
|
if string(resp.Body()) != "ok" {
|
||||||
return errors.New("not ok")
|
return errors.New("not ok")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
|||||||
ActualAmount: 1,
|
ActualAmount: 1,
|
||||||
ReceiveAddress: "wallet_1",
|
ReceiveAddress: "wallet_1",
|
||||||
Token: "USDT",
|
Token: "USDT",
|
||||||
|
Network: "tron",
|
||||||
Status: mdb.StatusWaitPay,
|
Status: mdb.StatusWaitPay,
|
||||||
NotifyUrl: "https://merchant.example/callback",
|
NotifyUrl: "https://merchant.example/callback",
|
||||||
}
|
}
|
||||||
@@ -36,7 +37,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
|||||||
if err := dao.Mdb.Model(order).UpdateColumn("created_at", time.Now().Add(-20*time.Minute)).Error; err != nil {
|
if err := dao.Mdb.Model(order).UpdateColumn("created_at", time.Now().Add(-20*time.Minute)).Error; err != nil {
|
||||||
t.Fatalf("age expired order: %v", err)
|
t.Fatalf("age expired order: %v", err)
|
||||||
}
|
}
|
||||||
if err := data.LockTransaction(order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil {
|
if err := data.LockTransaction("tron", order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil {
|
||||||
t.Fatalf("lock expired order: %v", err)
|
t.Fatalf("lock expired order: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +49,14 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
|||||||
ActualAmount: 1.01,
|
ActualAmount: 1.01,
|
||||||
ReceiveAddress: "wallet_1",
|
ReceiveAddress: "wallet_1",
|
||||||
Token: "USDT",
|
Token: "USDT",
|
||||||
|
Network: "tron",
|
||||||
Status: mdb.StatusWaitPay,
|
Status: mdb.StatusWaitPay,
|
||||||
NotifyUrl: "https://merchant.example/callback",
|
NotifyUrl: "https://merchant.example/callback",
|
||||||
}
|
}
|
||||||
if err := dao.Mdb.Create(recentOrder).Error; err != nil {
|
if err := dao.Mdb.Create(recentOrder).Error; err != nil {
|
||||||
t.Fatalf("create recent order: %v", err)
|
t.Fatalf("create recent order: %v", err)
|
||||||
}
|
}
|
||||||
if err := data.LockTransaction(recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.TradeId, recentOrder.ActualAmount, time.Hour); err != nil {
|
if err := data.LockTransaction("tron", recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.TradeId, recentOrder.ActualAmount, time.Hour); err != nil {
|
||||||
t.Fatalf("lock recent order: %v", err)
|
t.Fatalf("lock recent order: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
|||||||
if expired.Status != mdb.StatusExpired {
|
if expired.Status != mdb.StatusExpired {
|
||||||
t.Fatalf("expired order status = %d, want %d", expired.Status, mdb.StatusExpired)
|
t.Fatalf("expired order status = %d, want %d", expired.Status, mdb.StatusExpired)
|
||||||
}
|
}
|
||||||
lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(order.ReceiveAddress, order.Token, order.ActualAmount)
|
lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken("tron", order.ReceiveAddress, order.Token, order.ActualAmount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expired order lock lookup: %v", err)
|
t.Fatalf("expired order lock lookup: %v", err)
|
||||||
}
|
}
|
||||||
@@ -82,7 +84,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
|||||||
if recent.Status != mdb.StatusWaitPay {
|
if recent.Status != mdb.StatusWaitPay {
|
||||||
t.Fatalf("recent order status = %d, want %d", recent.Status, mdb.StatusWaitPay)
|
t.Fatalf("recent order status = %d, want %d", recent.Status, mdb.StatusWaitPay)
|
||||||
}
|
}
|
||||||
lockTradeID, err = data.GetTradeIdByWalletAddressAndAmountAndToken(recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.ActualAmount)
|
lockTradeID, err = data.GetTradeIdByWalletAddressAndAmountAndToken("tron", recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.ActualAmount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("recent order lock lookup: %v", err)
|
t.Fatalf("recent order lock lookup: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-1
@@ -3,11 +3,17 @@ package route
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/assimon/luuu/config"
|
||||||
"github.com/assimon/luuu/controller/comm"
|
"github.com/assimon/luuu/controller/comm"
|
||||||
"github.com/assimon/luuu/middleware"
|
"github.com/assimon/luuu/middleware"
|
||||||
|
"github.com/assimon/luuu/model/mdb"
|
||||||
|
"github.com/assimon/luuu/util/constant"
|
||||||
|
"github.com/assimon/luuu/util/sign"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,6 +26,7 @@ func RegisterRoute(e *echo.Echo) {
|
|||||||
payRoute := e.Group("/pay")
|
payRoute := e.Group("/pay")
|
||||||
payRoute.GET("/checkout-counter/:trade_id", comm.Ctrl.CheckoutCounter)
|
payRoute.GET("/checkout-counter/:trade_id", comm.Ctrl.CheckoutCounter)
|
||||||
payRoute.GET("/check-status/:trade_id", comm.Ctrl.CheckStatus)
|
payRoute.GET("/check-status/:trade_id", comm.Ctrl.CheckStatus)
|
||||||
|
payRoute.POST("/switch-network", comm.Ctrl.SwitchNetwork)
|
||||||
|
|
||||||
// payment routes
|
// payment routes
|
||||||
paymentRoute := e.Group("/payments")
|
paymentRoute := e.Group("/payments")
|
||||||
@@ -40,7 +47,7 @@ func RegisterRoute(e *echo.Echo) {
|
|||||||
body["currency"] = "cny"
|
body["currency"] = "cny"
|
||||||
}
|
}
|
||||||
if _, ok := body["network"]; !ok {
|
if _, ok := body["network"]; !ok {
|
||||||
body["network"] = "TRON"
|
body["network"] = "tron"
|
||||||
}
|
}
|
||||||
ctx.Set("request_body", body)
|
ctx.Set("request_body", body)
|
||||||
|
|
||||||
@@ -49,6 +56,7 @@ func RegisterRoute(e *echo.Echo) {
|
|||||||
return comm.Ctrl.FailJson(ctx, err)
|
return comm.Ctrl.FailJson(ctx, err)
|
||||||
}
|
}
|
||||||
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes))
|
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes))
|
||||||
|
ctx.Request().ContentLength = int64(len(jsonBytes))
|
||||||
|
|
||||||
return comm.Ctrl.CreateTransaction(ctx)
|
return comm.Ctrl.CreateTransaction(ctx)
|
||||||
}, middleware.CheckApiSign())
|
}, middleware.CheckApiSign())
|
||||||
@@ -57,4 +65,73 @@ func RegisterRoute(e *echo.Echo) {
|
|||||||
gmpayV1 := paymentRoute.Group("/gmpay/v1")
|
gmpayV1 := paymentRoute.Group("/gmpay/v1")
|
||||||
gmpayV1.POST("/order/create-transaction", comm.Ctrl.CreateTransaction, middleware.CheckApiSign())
|
gmpayV1.POST("/order/create-transaction", comm.Ctrl.CreateTransaction, middleware.CheckApiSign())
|
||||||
|
|
||||||
|
// wallet management routes
|
||||||
|
walletV1 := gmpayV1.Group("/wallet", middleware.CheckApiToken())
|
||||||
|
walletV1.POST("/add", comm.Ctrl.AddWallet)
|
||||||
|
walletV1.GET("/list", comm.Ctrl.ListWallets)
|
||||||
|
walletV1.GET("/:id", comm.Ctrl.GetWallet)
|
||||||
|
walletV1.POST("/:id/status", comm.Ctrl.ChangeWalletStatus)
|
||||||
|
walletV1.POST("/:id/delete", comm.Ctrl.DeleteWallet)
|
||||||
|
|
||||||
|
// epay v1 routes
|
||||||
|
epayV1 := paymentRoute.Group("/epay/v1")
|
||||||
|
epayV1.GET("/order/create-transaction/submit.php", func(ctx echo.Context) error {
|
||||||
|
money := ctx.QueryParam("money")
|
||||||
|
name := ctx.QueryParam("name")
|
||||||
|
notifyURL := ctx.QueryParam("notify_url")
|
||||||
|
outTradeNo := ctx.QueryParam("out_trade_no")
|
||||||
|
returnURL := ctx.QueryParam("return_url")
|
||||||
|
signstr := ctx.QueryParam("sign")
|
||||||
|
// signType := ctx.QueryParam("sign_type")
|
||||||
|
|
||||||
|
m := map[string]interface{}{
|
||||||
|
"money": money,
|
||||||
|
"name": name,
|
||||||
|
"notify_url": notifyURL,
|
||||||
|
"out_trade_no": outTradeNo,
|
||||||
|
"pid": config.GetEpayPid(), // 注意:验签时需要包含 pid 参数
|
||||||
|
"return_url": returnURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSignature, err := sign.Get(m, config.GetApiAuthToken())
|
||||||
|
if err != nil {
|
||||||
|
return constant.SignatureErr
|
||||||
|
}
|
||||||
|
if checkSignature != signstr {
|
||||||
|
return constant.SignatureErr
|
||||||
|
}
|
||||||
|
|
||||||
|
amountFloat, err := strconv.ParseFloat(money, 64)
|
||||||
|
if err != nil {
|
||||||
|
return comm.Ctrl.FailJson(ctx, fmt.Errorf("invalid money value: %s", money))
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"token": "usdt",
|
||||||
|
"currency": "cny",
|
||||||
|
"network": "tron",
|
||||||
|
"amount": amountFloat,
|
||||||
|
"notify_url": notifyURL,
|
||||||
|
"order_id": outTradeNo,
|
||||||
|
"redirect_url": returnURL,
|
||||||
|
"signature": signstr,
|
||||||
|
"name": name,
|
||||||
|
"payment_type": mdb.PaymentTypeEpay,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Set("request_body", body)
|
||||||
|
|
||||||
|
jsonBytes, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return comm.Ctrl.FailJson(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes))
|
||||||
|
ctx.Request().ContentLength = int64(len(jsonBytes))
|
||||||
|
ctx.Request().Method = http.MethodPost
|
||||||
|
ctx.Request().Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return comm.Ctrl.CreateTransactionAndRedirect(ctx)
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/assimon/luuu/model/dao"
|
||||||
|
"github.com/assimon/luuu/model/mdb"
|
||||||
|
"github.com/assimon/luuu/util/log"
|
||||||
|
"github.com/assimon/luuu/util/sign"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testAPIToken = "test-secret-token"
|
||||||
|
|
||||||
|
func setupTestEnv(t *testing.T) *echo.Echo {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// minimal viper config
|
||||||
|
viper.Reset()
|
||||||
|
viper.Set("db_type", "sqlite")
|
||||||
|
viper.Set("api_auth_token", testAPIToken)
|
||||||
|
viper.Set("app_uri", "http://localhost:8080")
|
||||||
|
viper.Set("order_expiration_time", 10)
|
||||||
|
viper.Set("api_rate_url", "")
|
||||||
|
viper.Set("forced_usdt_rate", 7.0)
|
||||||
|
viper.Set("runtime_root_path", tmpDir)
|
||||||
|
viper.Set("log_save_path", tmpDir)
|
||||||
|
viper.Set("sqlite_database_filename", tmpDir+"/test.db")
|
||||||
|
viper.Set("runtime_sqlite_filename", tmpDir+"/runtime.db")
|
||||||
|
|
||||||
|
log.Init()
|
||||||
|
|
||||||
|
// init config paths
|
||||||
|
os.Setenv("EPUSDT_CONFIG", tmpDir)
|
||||||
|
defer os.Unsetenv("EPUSDT_CONFIG")
|
||||||
|
|
||||||
|
// init DB
|
||||||
|
if err := dao.DBInit(); err != nil {
|
||||||
|
t.Fatalf("DBInit: %v", err)
|
||||||
|
}
|
||||||
|
if err := dao.RuntimeInit(); err != nil {
|
||||||
|
t.Fatalf("RuntimeInit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure tables exist (MdbTableInit uses sync.Once, so migrate directly)
|
||||||
|
dao.Mdb.AutoMigrate(&mdb.Orders{}, &mdb.WalletAddress{})
|
||||||
|
|
||||||
|
// seed wallet addresses
|
||||||
|
dao.Mdb.Create(&mdb.WalletAddress{Network: mdb.NetworkTron, Address: "TTestTronAddress001", Status: mdb.TokenStatusEnable})
|
||||||
|
dao.Mdb.Create(&mdb.WalletAddress{Network: mdb.NetworkSolana, Address: "SolTestAddress001", Status: mdb.TokenStatusEnable})
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
RegisterRoute(e)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func signBody(body map[string]interface{}) map[string]interface{} {
|
||||||
|
sig, _ := sign.Get(body, testAPIToken)
|
||||||
|
body["signature"] = sig
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
func doPost(e *echo.Echo, path string, body map[string]interface{}) *httptest.ResponseRecorder {
|
||||||
|
jsonBytes, _ := json.Marshal(body)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(string(jsonBytes)))
|
||||||
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateOrderEpusdtDefaultTron tests the epusdt compatibility route defaults to tron network.
|
||||||
|
func TestCreateOrderEpusdtDefaultTron(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
body := signBody(map[string]interface{}{
|
||||||
|
"order_id": "test-tron-001",
|
||||||
|
"amount": 1.00,
|
||||||
|
"notify_url": "http://localhost/notify",
|
||||||
|
})
|
||||||
|
|
||||||
|
rec := doPost(e, "/payments/epusdt/v1/order/create-transaction", body)
|
||||||
|
t.Logf("Status: %d, Body: %s", rec.Code, rec.Body.String())
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]interface{}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := resp["data"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected data in response, got: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data["trade_id"] == nil || data["trade_id"] == "" {
|
||||||
|
t.Error("expected trade_id in response")
|
||||||
|
}
|
||||||
|
if data["receive_address"] != "TTestTronAddress001" {
|
||||||
|
t.Errorf("expected tron address, got: %v", data["receive_address"])
|
||||||
|
}
|
||||||
|
t.Logf("Order created: trade_id=%v address=%v amount=%v", data["trade_id"], data["receive_address"], data["actual_amount"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateOrderGmpayV1Solana tests the gmpay route with solana network.
|
||||||
|
func TestCreateOrderGmpayV1Solana(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
body := signBody(map[string]interface{}{
|
||||||
|
"order_id": "test-sol-001",
|
||||||
|
"amount": 1.00,
|
||||||
|
"token": "usdt",
|
||||||
|
"currency": "cny",
|
||||||
|
"network": "solana",
|
||||||
|
"notify_url": "http://localhost/notify",
|
||||||
|
})
|
||||||
|
|
||||||
|
rec := doPost(e, "/payments/gmpay/v1/order/create-transaction", body)
|
||||||
|
t.Logf("Status: %d, Body: %s", rec.Code, rec.Body.String())
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]interface{}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := resp["data"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected data in response, got: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data["trade_id"] == nil || data["trade_id"] == "" {
|
||||||
|
t.Error("expected trade_id in response")
|
||||||
|
}
|
||||||
|
if data["receive_address"] != "SolTestAddress001" {
|
||||||
|
t.Errorf("expected solana address, got: %v", data["receive_address"])
|
||||||
|
}
|
||||||
|
t.Logf("Order created: trade_id=%v address=%v amount=%v", data["trade_id"], data["receive_address"], data["actual_amount"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateOrderGmpayV1SolNative tests creating an order for native SOL token.
|
||||||
|
func TestCreateOrderGmpayV1SolNative(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
body := signBody(map[string]interface{}{
|
||||||
|
"order_id": "test-sol-native-001",
|
||||||
|
"amount": 0.05,
|
||||||
|
"token": "sol",
|
||||||
|
"currency": "usd",
|
||||||
|
"network": "solana",
|
||||||
|
"notify_url": "http://localhost/notify",
|
||||||
|
})
|
||||||
|
|
||||||
|
rec := doPost(e, "/payments/gmpay/v1/order/create-transaction", body)
|
||||||
|
t.Logf("Status: %d, Body: %s", rec.Code, rec.Body.String())
|
||||||
|
|
||||||
|
var resp map[string]interface{}
|
||||||
|
json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||||
|
t.Logf("Response: %v", resp)
|
||||||
|
|
||||||
|
// This may fail if rate API is not configured, which is expected in test
|
||||||
|
// The important thing is the route accepts the request with network=solana token=sol
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Logf("Note: non-200 may be expected if rate API is not configured for SOL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doGet(e *echo.Echo, path string) *httptest.ResponseRecorder {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
req.Header.Set("Authorization", testAPIToken)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
func doPostWithToken(e *echo.Echo, path string, body map[string]interface{}) *httptest.ResponseRecorder {
|
||||||
|
jsonBytes, _ := json.Marshal(body)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(string(jsonBytes)))
|
||||||
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||||
|
req.Header.Set("Authorization", testAPIToken)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResp(t *testing.T, rec *httptest.ResponseRecorder) map[string]interface{} {
|
||||||
|
t.Helper()
|
||||||
|
var resp map[string]interface{}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWalletAddAndList tests adding wallets via API and listing them.
|
||||||
|
func TestWalletAddAndList(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
// Add a solana wallet
|
||||||
|
rec := doPostWithToken(e, "/payments/gmpay/v1/wallet/add", map[string]interface{}{
|
||||||
|
"network": "solana",
|
||||||
|
"address": "NewSolWallet001",
|
||||||
|
})
|
||||||
|
t.Logf("Add: %s", rec.Body.String())
|
||||||
|
resp := parseResp(t, rec)
|
||||||
|
if resp["status_code"].(float64) != 200 {
|
||||||
|
t.Fatalf("add wallet failed: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a tron wallet
|
||||||
|
rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/add", map[string]interface{}{
|
||||||
|
"network": "tron",
|
||||||
|
"address": "NewTronWallet001",
|
||||||
|
})
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
if resp["status_code"].(float64) != 200 {
|
||||||
|
t.Fatalf("add tron wallet failed: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all wallets
|
||||||
|
rec = doGet(e, "/payments/gmpay/v1/wallet/list")
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
wallets := resp["data"].([]interface{})
|
||||||
|
// 2 seeded + 2 added = 4
|
||||||
|
if len(wallets) != 4 {
|
||||||
|
t.Fatalf("expected 4 wallets, got %d: %v", len(wallets), wallets)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List by network
|
||||||
|
rec = doGet(e, "/payments/gmpay/v1/wallet/list?network=solana")
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
wallets = resp["data"].([]interface{})
|
||||||
|
if len(wallets) != 2 {
|
||||||
|
t.Fatalf("expected 2 solana wallets, got %d", len(wallets))
|
||||||
|
}
|
||||||
|
|
||||||
|
rec = doGet(e, "/payments/gmpay/v1/wallet/list?network=tron")
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
wallets = resp["data"].([]interface{})
|
||||||
|
if len(wallets) != 2 {
|
||||||
|
t.Fatalf("expected 2 tron wallets, got %d", len(wallets))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWalletDuplicateRejected tests that adding the same network+address twice fails.
|
||||||
|
func TestWalletDuplicateRejected(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
body := map[string]interface{}{"network": "solana", "address": "DupWallet001"}
|
||||||
|
rec := doPostWithToken(e, "/payments/gmpay/v1/wallet/add", body)
|
||||||
|
resp := parseResp(t, rec)
|
||||||
|
if resp["status_code"].(float64) != 200 {
|
||||||
|
t.Fatalf("first add failed: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same network+address should fail
|
||||||
|
rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/add", body)
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
if resp["status_code"].(float64) == 200 {
|
||||||
|
t.Fatal("expected duplicate to be rejected")
|
||||||
|
}
|
||||||
|
t.Logf("Duplicate rejected: %v", resp["message"])
|
||||||
|
|
||||||
|
// Same address, different network should succeed
|
||||||
|
rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/add", map[string]interface{}{
|
||||||
|
"network": "tron",
|
||||||
|
"address": "DupWallet001",
|
||||||
|
})
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
if resp["status_code"].(float64) != 200 {
|
||||||
|
t.Fatalf("same address on different network should succeed: %v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWalletStatusAndDelete tests enable/disable/delete operations.
|
||||||
|
func TestWalletStatusAndDelete(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
// Add a wallet
|
||||||
|
rec := doPostWithToken(e, "/payments/gmpay/v1/wallet/add", map[string]interface{}{
|
||||||
|
"network": "solana",
|
||||||
|
"address": "StatusTestWallet",
|
||||||
|
})
|
||||||
|
resp := parseResp(t, rec)
|
||||||
|
wallet := resp["data"].(map[string]interface{})
|
||||||
|
walletID := fmt.Sprintf("%.0f", wallet["id"].(float64))
|
||||||
|
|
||||||
|
// Get wallet
|
||||||
|
rec = doGet(e, "/payments/gmpay/v1/wallet/"+walletID)
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
if resp["status_code"].(float64) != 200 {
|
||||||
|
t.Fatalf("get wallet failed: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable wallet
|
||||||
|
rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/"+walletID+"/status", map[string]interface{}{
|
||||||
|
"status": 2,
|
||||||
|
})
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
if resp["status_code"].(float64) != 200 {
|
||||||
|
t.Fatalf("disable wallet failed: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify disabled — should not appear in available list
|
||||||
|
rec = doGet(e, "/payments/gmpay/v1/wallet/list?network=solana")
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
wallets := resp["data"].([]interface{})
|
||||||
|
for _, w := range wallets {
|
||||||
|
wm := w.(map[string]interface{})
|
||||||
|
if wm["address"] == "StatusTestWallet" && wm["status"].(float64) != 2 {
|
||||||
|
t.Error("wallet should be disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete wallet
|
||||||
|
rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/"+walletID+"/delete", nil)
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
if resp["status_code"].(float64) != 200 {
|
||||||
|
t.Fatalf("delete wallet failed: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify deleted
|
||||||
|
rec = doGet(e, "/payments/gmpay/v1/wallet/"+walletID)
|
||||||
|
resp = parseResp(t, rec)
|
||||||
|
// Should return not found
|
||||||
|
if resp["status_code"].(float64) == 200 {
|
||||||
|
data := resp["data"].(map[string]interface{})
|
||||||
|
if data["id"].(float64) > 0 {
|
||||||
|
t.Error("wallet should be deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWalletAuthRequired tests that wallet APIs require auth token.
|
||||||
|
func TestWalletAuthRequired(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
// No auth header — should not return success
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/wallet/list", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// The response should indicate auth failure (not 200 success)
|
||||||
|
var resp map[string]interface{}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
// echo may return plain text error
|
||||||
|
if rec.Code == http.StatusOK {
|
||||||
|
t.Error("expected auth failure without token")
|
||||||
|
}
|
||||||
|
t.Logf("Auth rejected (non-JSON): status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statusCode, _ := resp["status_code"].(float64)
|
||||||
|
if statusCode == 200 {
|
||||||
|
t.Error("expected auth failure without token")
|
||||||
|
}
|
||||||
|
t.Logf("Auth rejected: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateOrderNetworkIsolation verifies tron and solana wallets don't mix.
|
||||||
|
func TestCreateOrderNetworkIsolation(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
// Try to create a solana order — should get solana address, not tron
|
||||||
|
body := signBody(map[string]interface{}{
|
||||||
|
"order_id": fmt.Sprintf("test-isolation-%d", 1),
|
||||||
|
"amount": 1.00,
|
||||||
|
"token": "usdt",
|
||||||
|
"currency": "cny",
|
||||||
|
"network": "solana",
|
||||||
|
"notify_url": "http://localhost/notify",
|
||||||
|
})
|
||||||
|
rec := doPost(e, "/payments/gmpay/v1/order/create-transaction", body)
|
||||||
|
|
||||||
|
var resp map[string]interface{}
|
||||||
|
json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||||
|
|
||||||
|
data, ok := resp["data"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected data, got: %v", resp)
|
||||||
|
}
|
||||||
|
if data["receive_address"] == "TTestTronAddress001" {
|
||||||
|
t.Error("solana order should NOT get a tron address")
|
||||||
|
}
|
||||||
|
if data["receive_address"] != "SolTestAddress001" {
|
||||||
|
t.Errorf("expected SolTestAddress001, got %v", data["receive_address"])
|
||||||
|
}
|
||||||
|
}
|
||||||
+415
-139
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="dark">
|
<html lang="en" data-theme="dark">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@@ -23,236 +24,508 @@
|
|||||||
--color-popover-foreground:var(--popover-foreground);
|
--color-popover-foreground:var(--popover-foreground);
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
--color-primary-foreground:var(--primary-foreground);
|
--color-primary-foreground:var(--primary-foreground);
|
||||||
--color-primary-hover: var(--primary-hover);
|
|
||||||
--color-secondary: var(--secondary);
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground:var(--secondary-foreground);
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-border-hover: var(--border-hover);
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
--color-success: var(--success);
|
--color-success: var(--success);
|
||||||
--color-warning: var(--warning);
|
--color-warning: var(--warning);
|
||||||
--shadow-card: var(--card-shadow);
|
|
||||||
--shadow-popover: var(--popover-shadow);
|
|
||||||
--animate-state-in: state-in .28s cubic-bezier(.34,1.56,.64,1);
|
--animate-state-in: state-in .28s cubic-bezier(.34,1.56,.64,1);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Design tokens ── */
|
||||||
|
:root, [data-theme="light"] {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground:oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.21 0.006 285.885);
|
||||||
|
--primary-foreground:oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground:oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.015 286.067);
|
||||||
|
--radius: 0.875rem;
|
||||||
|
--success: oklch(0.721 0.193 143.56);
|
||||||
|
--warning: oklch(0.741 0.188 55.68);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground:oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.92 0.004 286.32);
|
||||||
|
--primary-foreground:oklch(0.21 0.006 285.885);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground:oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
|
--success: oklch(0.747 0.201 143.5);
|
||||||
|
--warning: oklch(0.762 0.182 61.47);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Ambient blobs ── */
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fill-bar {
|
||||||
|
from { width: 0%; }
|
||||||
|
to { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-out-l {
|
||||||
|
from { transform: translateX(0); }
|
||||||
|
to { transform: translateX(calc(-100% - 20px)); }
|
||||||
|
}
|
||||||
|
@keyframes slide-out-r {
|
||||||
|
from { transform: translateX(0); }
|
||||||
|
to { transform: translateX(calc(100% + 20px)); }
|
||||||
|
}
|
||||||
|
@keyframes slide-in-r {
|
||||||
|
from { transform: translateX(calc(100% + 20px)); }
|
||||||
|
to { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
@keyframes slide-in-l {
|
||||||
|
from { transform: translateX(calc(-100% - 20px)); }
|
||||||
|
to { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blob-drift {
|
||||||
|
0% { transform: translate(0,0) scale(1); }
|
||||||
|
50% { transform: translate(30px,-20px) scale(1.06); }
|
||||||
|
100% { transform: translate(-20px,30px) scale(0.96); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* Card */
|
||||||
|
.card {
|
||||||
|
@apply bg-card border border-border rounded-2xl shadow-md transition-colors duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chip / pill button */
|
||||||
|
.chip {
|
||||||
|
@apply bg-card border border-border rounded-full px-3.5 py-1.5 text-card-foreground shadow-xs transition-all;
|
||||||
|
}
|
||||||
|
.chip:hover { @apply bg-accent border-ring; }
|
||||||
|
.chip:active { @apply opacity-60; }
|
||||||
|
.chip.w-9 { @apply p-0; }
|
||||||
|
|
||||||
|
/* Copy rows */
|
||||||
|
.row {
|
||||||
|
@apply flex items-center px-4 py-3.5 transition-colors;
|
||||||
|
}
|
||||||
|
.row:active { @apply bg-muted; }
|
||||||
|
|
||||||
|
/* Icon button */
|
||||||
|
.icon-btn {
|
||||||
|
@apply flex items-center justify-center size-8 rounded-sm text-muted-foreground bg-secondary border border-transparent shrink-0 transition-colors;
|
||||||
|
}
|
||||||
|
.icon-btn:hover { @apply bg-accent text-card-foreground border-border; }
|
||||||
|
|
||||||
|
/* Primary button */
|
||||||
|
.btn-primary {
|
||||||
|
@apply flex items-center justify-center bg-primary text-primary-foreground border border-border rounded-xl px-6 py-3 text-base font-semibold tracking-tight cursor-pointer shadow-md transition-[background,border-color,transform,opacity];
|
||||||
|
}
|
||||||
|
.btn-primary:hover { @apply bg-primary/90; }
|
||||||
|
.btn-primary:active { @apply scale-[0.97] opacity-90; }
|
||||||
|
.btn-primary:disabled { @apply opacity-45 cursor-not-allowed scale-100; }
|
||||||
|
|
||||||
|
/* Secondary button */
|
||||||
|
.btn-secondary {
|
||||||
|
@apply inline-flex items-center justify-center bg-card text-card-foreground border border-border rounded-xl px-7 py-3 text-base font-medium cursor-pointer shadow-sm transition-all;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { @apply bg-accent border-ring; }
|
||||||
|
.btn-secondary:active { @apply opacity-70; }
|
||||||
|
|
||||||
|
/* State icon circle */
|
||||||
|
.state-icon {
|
||||||
|
@apply size-20 rounded-full flex items-center justify-center mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown menu */
|
||||||
|
.menu {
|
||||||
|
@apply bg-popover border border-border rounded-xl shadow-xl overflow-hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.menu--up { transform: translateY(8px); }
|
||||||
|
.menu.is-open { opacity: 1; transform: translateY(0); pointer-events: all; }
|
||||||
|
.menu-item {
|
||||||
|
@apply flex items-center gap-2.5 px-4 py-2.5 text-sm font-medium text-card-foreground cursor-pointer transition-colors;
|
||||||
|
}
|
||||||
|
.menu-item:hover { @apply bg-accent; }
|
||||||
|
.menu-item.is-selected { @apply text-primary font-semibold; }
|
||||||
|
.menu-item + .menu-item { @apply border-t border-border; }
|
||||||
|
.select-trigger.is-open { opacity: 0.8; }
|
||||||
|
.select-trigger.is-open .select-chevron { transform: rotate(180deg); }
|
||||||
|
|
||||||
|
/* QR wrapper */
|
||||||
|
.qr-wrapper {
|
||||||
|
@apply bg-white rounded-xl p-3.5 border border-border shadow-md inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timer SVG rings */
|
||||||
|
.timer-ring-track { stroke: var(--border); transition: stroke .3s; }
|
||||||
|
.timer-ring-progress { transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dashoffset 1s linear, stroke .5s; }
|
||||||
|
.timer-icon { @apply text-muted-foreground; }
|
||||||
|
|
||||||
|
/* Flippable status card */
|
||||||
|
.status-card {
|
||||||
|
position: relative;
|
||||||
|
perspective: 1600px;
|
||||||
|
transition: height 0.38s cubic-bezier(.22,1,.36,1);
|
||||||
|
}
|
||||||
|
.status-card-inner {
|
||||||
|
position: relative; width: 100%;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: transform 0.82s cubic-bezier(.22,.9,.24,1);
|
||||||
|
}
|
||||||
|
.status-card.is-flipped .status-card-inner { transform: rotateY(180deg); }
|
||||||
|
.status-face {
|
||||||
|
@apply absolute inset-0 w-full;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
.status-face-front { z-index: 2; }
|
||||||
|
.status-face-back { transform: rotateY(180deg); }
|
||||||
|
.state-panel-shell { @apply pt-px; }
|
||||||
|
.state-screen { @apply hidden; }
|
||||||
|
.state-screen.is-active { @apply flex; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Panel viewport: hide scrollbars ── */
|
||||||
|
#panel-viewport { scrollbar-width: none; }
|
||||||
|
#panel-viewport::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
/* ── Toast ── */
|
||||||
|
#toast {
|
||||||
|
@apply fixed left-1/2 bottom-9 text-sm font-medium px-5 py-2.5 rounded-xl border border-border shadow-xl opacity-0 pointer-events-none whitespace-nowrap z-9999;
|
||||||
|
@apply bg-foreground/95 text-background;
|
||||||
|
@apply dark:bg-card/95 dark:text-card-foreground;
|
||||||
|
transform: translateX(-50%) translateY(12px);
|
||||||
|
transition: opacity 0.22s, transform 0.22s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes state-in {
|
||||||
|
from { opacity: 0; transform: translateY(12px) scale(0.96); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
|
||||||
|
::view-transition-old(root) { z-index: 1; }
|
||||||
|
::view-transition-new(root) { z-index: 9; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.status-card, .status-card-inner, .status-face, .timer-ring-progress {
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/static/payment.css" />
|
|
||||||
<script defer src="https://cdn.jsdmirror.com/npm/lucide@latest/dist/umd/lucide.min.js"></script>
|
<script defer src="https://cdn.jsdmirror.com/npm/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||||
<script defer src="https://cdn.jsdmirror.com/npm/qrcodejs@latest/qrcode.min.js"></script>
|
<script defer src="https://cdn.jsdmirror.com/npm/qrcodejs@latest/qrcode.min.js"></script>
|
||||||
<script defer src="https://cdn.jsdmirror.com/npm/clipboard@latest/dist/clipboard.min.js"></script>
|
<script defer src="https://cdn.jsdmirror.com/npm/clipboard@latest/dist/clipboard.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background text-foreground min-h-screen flex flex-col items-center justify-center font-sans transition-colors duration-300">
|
|
||||||
|
|
||||||
<!-- Ambient background blobs -->
|
|
||||||
<div class="fixed inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
|
||||||
<div class="blob blob-1"></div>
|
|
||||||
<div class="blob blob-2"></div>
|
|
||||||
<div class="blob blob-3"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-center w-full max-w-[400px] px-4 py-8 relative z-10">
|
|
||||||
|
|
||||||
|
<body
|
||||||
|
class="bg-background text-foreground min-h-screen flex flex-col items-center font-sans transition-colors duration-300">
|
||||||
<!-- Nav bar -->
|
<!-- Nav bar -->
|
||||||
<div class="w-full flex items-center justify-between mb-6">
|
<header class="w-full max-w-sm px-4 pt-8 pb-6 flex items-center justify-between relative z-10">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<img src="https://www.gmwallet.app/favicon.png" alt="logo" class="h-8 w-8 rounded-[10px] shadow-sm" />
|
<img src="https://www.gmwallet.app/favicon.png" alt="logo" class="h-8 w-8 rounded-sm shadow-sm" />
|
||||||
<span class="text-[17px] font-semibold tracking-[-0.3px] text-card-foreground">GM Pay</span>
|
<span class="text-lg font-semibold tracking-tight text-card-foreground">GM Pay</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Language pill -->
|
<!-- Language pill -->
|
||||||
<div class="select-wrap relative select-none" id="dd-lang">
|
<div class="select-wrap relative select-none" id="dd-lang">
|
||||||
<div class="select-trigger ios-pill flex items-center gap-1.5 cursor-pointer" onclick="toggleSelect('dd-lang')">
|
<div class="select-trigger chip flex items-center gap-1.5 cursor-pointer" onclick="toggleSelect('dd-lang')">
|
||||||
<span class="text-[13px] font-medium" id="lang-label">EN</span>
|
<span class="text-sm font-medium" id="lang-label">EN</span>
|
||||||
<i data-lucide="chevron-down" class="select-chevron text-muted-foreground shrink-0 transition-transform duration-200" width="11" height="11" stroke-width="2.5"></i>
|
<i data-lucide="chevron-down"
|
||||||
|
class="select-chevron text-muted-foreground shrink-0 transition-transform duration-200" width="11"
|
||||||
|
height="11" stroke-width="2.5"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="select-menu ios-menu absolute z-100 top-[calc(100%+12px)] right-0 min-w-[160px] overflow-hidden opacity-0 -translate-y-2 pointer-events-none transition-all duration-200" id="dd-lang-menu">
|
<div class="select-menu menu absolute z-100 top-[calc(100%+12px)] right-0 min-w-40" id="dd-lang-menu">
|
||||||
<div class="select-option ios-menu-item is-selected" data-lang="en" onclick="setLang('en')">🇺🇸 English</div>
|
<div class="select-option menu-item is-selected" data-lang="en" onclick="setLang('en')">🇺🇸 English
|
||||||
<div class="select-option ios-menu-item" data-lang="zh" onclick="setLang('zh')">🇨🇳 中文</div>
|
</div>
|
||||||
<div class="select-option ios-menu-item" data-lang="ja" onclick="setLang('ja')">🇯🇵 日本語</div>
|
<div class="select-option menu-item" data-lang="zh" onclick="setLang('zh')">🇨🇳 中文</div>
|
||||||
<div class="select-option ios-menu-item" data-lang="ko" onclick="setLang('ko')">🇰🇷 한국어</div>
|
<div class="select-option menu-item" data-lang="ja" onclick="setLang('ja')">🇯🇵 日本語</div>
|
||||||
<div class="select-option ios-menu-item" data-lang="zh-hk" onclick="setLang('zh-hk')">🇭🇰 繁體中文</div>
|
<div class="select-option menu-item" data-lang="ko" onclick="setLang('ko')">🇰🇷 한국어</div>
|
||||||
<div class="select-option ios-menu-item" data-lang="ru" onclick="setLang('ru')">🇷🇺 Русский</div>
|
<div class="select-option menu-item" data-lang="zh-hk" onclick="setLang('zh-hk')">🇭🇰 繁體中文</div>
|
||||||
|
<div class="select-option menu-item" data-lang="ru" onclick="setLang('ru')">🇷🇺 Русский</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Theme toggle -->
|
<!-- Theme toggle -->
|
||||||
<button class="ios-pill w-9 h-9 flex items-center justify-center cursor-pointer" onclick="toggleTheme(event)" title="Toggle theme">
|
<button class="chip w-9 h-9 flex items-center justify-center cursor-pointer" onclick="toggleTheme(event)"
|
||||||
|
title="Toggle theme">
|
||||||
<i data-lucide="moon" id="icon-moon" class="hidden" width="16" height="16" stroke-width="1.8"></i>
|
<i data-lucide="moon" id="icon-moon" class="hidden" width="16" height="16" stroke-width="1.8"></i>
|
||||||
<i data-lucide="sun" id="icon-sun" width="16" height="16" stroke-width="1.8"></i>
|
<i data-lucide="sun" id="icon-sun" width="16" height="16" stroke-width="1.8"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Ambient background blobs -->
|
||||||
|
<div class="fixed inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
||||||
|
<div
|
||||||
|
class="absolute rounded-full blur-[80px] animate-[blob-drift_18s_ease-in-out_infinite_alternate] size-[420px] -top-30 -left-25 bg-[color-mix(in_srgb,var(--muted)_60%,transparent)]">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute rounded-full blur-[80px] animate-[blob-drift_18s_ease-in-out_infinite_alternate] [animation-delay:-6s] size-[340px] -bottom-20 -right-15 bg-[color-mix(in_srgb,var(--success)_14%,transparent)]">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute rounded-full blur-[80px] animate-[blob-drift_18s_ease-in-out_infinite_alternate] [animation-delay:-12s] size-[260px] top-[45%] left-[55%] bg-[color-mix(in_srgb,var(--warning)_10%,transparent)]">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 flex flex-col items-center justify-center w-full max-w-sm px-5 relative">
|
||||||
|
|
||||||
|
<!-- Steps -->
|
||||||
|
<div id="step-progress" class="flex items-center gap-2 w-full mb-4 select-none">
|
||||||
|
<div id="step-bar-1" class="h-1 flex-1 rounded-full overflow-hidden"
|
||||||
|
style="background:color-mix(in srgb,var(--foreground) 10%,transparent)">
|
||||||
|
<div id="step-fill-1" class="h-full rounded-full" style="width:0%;background:var(--foreground)"></div>
|
||||||
|
</div>
|
||||||
|
<div id="step-bar-2" class="h-1 flex-1 rounded-full overflow-hidden"
|
||||||
|
style="background:color-mix(in srgb,var(--foreground) 10%,transparent)">
|
||||||
|
<div id="step-fill-2" class="h-full rounded-full" style="width:0%;background:var(--foreground)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hero amount card -->
|
<!-- Order info -->
|
||||||
<div id="order-info" class="glass-card w-full px-6 pt-6 pb-5 mb-3">
|
<aside id="order-info" class="card w-full px-6 pt-6 pb-5 mb-4">
|
||||||
<p class="text-[13px] font-medium text-muted-foreground mb-1" data-i18n="amount_to_pay">Amount to pay</p>
|
<p class="text-sm font-medium text-muted-foreground mb-1" data-i18n="amount_to_pay">Amount to pay</p>
|
||||||
<div class="flex items-end gap-3 justify-between">
|
<div class="flex items-end gap-3 justify-between">
|
||||||
<span class="text-[42px] font-bold leading-none tracking-[-0.04em] text-card-foreground font-nunito" id="display-amount">--</span>
|
<span class="text-4xl font-bold leading-none tracking-tighter text-card-foreground font-nunito"
|
||||||
<button class="ios-icon-btn mb-0.5 shrink-0" id="btn-copy-amount" title="Copy">
|
id="display-amount">--</span>
|
||||||
|
<button class="icon-btn mb-0.5 shrink-0" id="btn-copy-amount" title="Copy">
|
||||||
<i data-lucide="copy" width="15" height="15" stroke-width="2"></i>
|
<i data-lucide="copy" width="15" height="15" stroke-width="2"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 mt-1.5" id="display-network">--</div>
|
<div class="flex items-center gap-1.5 mt-1.5" id="display-network">--</div>
|
||||||
<div class="mt-3 pt-3 border-t border-border/50">
|
<div class="mt-3 pt-3 border-t border-border/50">
|
||||||
<table class="w-full text-[13px] text-muted-foreground border-separate border-spacing-y-1">
|
<table class="w-full text-sm text-muted-foreground border-separate border-spacing-y-1">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr id="display-fiat"></tr>
|
<tr id="display-fiat"></tr>
|
||||||
<tr id="display-order-id"></tr>
|
<tr id="display-order-id"></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Panel viewport: clips horizontal slide -->
|
||||||
|
<div class="relative w-full" id="panel-viewport">
|
||||||
|
|
||||||
|
<!-- Step 1: Select coin + network -->
|
||||||
|
<section id="step1-panel" class="w-full mb-4 pb-4">
|
||||||
|
<div class="flex gap-2 mb-4">
|
||||||
|
<!-- Network -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1.5"
|
||||||
|
data-i18n="network_label">Network</p>
|
||||||
|
<div class="select-wrap relative select-none" id="dd-network">
|
||||||
|
<div
|
||||||
|
class="select-trigger flex items-center justify-between bg-card border border-border rounded-xl px-3.5 py-3 cursor-pointer gap-2 shadow-sm transition-colors h-[50px]"
|
||||||
|
onclick="toggleSelect('dd-network')">
|
||||||
|
<span id="network-label" class="text-sm font-semibold text-card-foreground leading-none"></span>
|
||||||
|
<i data-lucide="chevron-down"
|
||||||
|
class="select-chevron text-muted-foreground shrink-0 transition-transform duration-200" width="13"
|
||||||
|
height="13" stroke-width="2.5"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="select-menu menu absolute z-100 top-[calc(100%+8px)] left-0 w-full" id="dd-network-menu">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Currency -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1.5"
|
||||||
|
data-i18n="currency_label">Currency</p>
|
||||||
|
<div class="select-wrap relative select-none" id="dd-token">
|
||||||
|
<div
|
||||||
|
class="select-trigger flex items-center justify-between bg-card border border-border rounded-xl px-3.5 py-3 cursor-pointer gap-2 shadow-sm transition-colors h-[50px]"
|
||||||
|
onclick="toggleSelect('dd-token')">
|
||||||
|
<span id="token-label" class="text-sm font-semibold text-card-foreground"></span>
|
||||||
|
<i data-lucide="chevron-down"
|
||||||
|
class="select-chevron text-muted-foreground shrink-0 transition-transform duration-200" width="13"
|
||||||
|
height="13" stroke-width="2.5"></i>
|
||||||
|
</div>
|
||||||
|
<div class="select-menu menu absolute z-100 top-[calc(100%+8px)] left-0 w-full" id="dd-token-menu">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="btn-confirm-step1" class="btn-primary w-full" onclick="confirmStep1()"
|
||||||
|
data-i18n="confirm">Confirm</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Flippable payment/state card -->
|
<!-- Payment panel (Step 2) -->
|
||||||
<div id="status-card" class="status-card w-full">
|
<section id="payment-panel" class="w-full pb-4" style="display:none">
|
||||||
<div class="status-card-inner">
|
<!-- QR + address card -->
|
||||||
<div class="status-face status-face-front">
|
<div class="card w-full px-5 pt-5 pb-0 mb-4 overflow-hidden">
|
||||||
<div id="payment-panel" class="w-full">
|
<!-- Scan title + timer -->
|
||||||
|
|
||||||
<!-- QR section -->
|
|
||||||
<div class="glass-card w-full px-5 py-5 mb-3">
|
|
||||||
<!-- Scan title + small ring -->
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<p class="text-[13px] font-semibold text-card-foreground" data-i18n="scan_title">Scan or copy address to pay</p>
|
<p class="text-sm font-semibold text-card-foreground" data-i18n="scan_title">Scan or copy address to pay
|
||||||
|
</p>
|
||||||
<div id="timer-row" class="relative w-10 h-10 shrink-0">
|
<div id="timer-row" class="relative w-10 h-10 shrink-0">
|
||||||
<svg class="w-10 h-10 absolute inset-0" viewBox="0 0 48 48">
|
<svg class="w-10 h-10 absolute inset-0" viewBox="0 0 48 48">
|
||||||
<circle class="timer-ring-track" cx="24" cy="24" r="20" fill="none" stroke-width="3"/>
|
<circle class="timer-ring-track" cx="24" cy="24" r="20" fill="none" stroke-width="3" />
|
||||||
<circle id="ring-track" class="timer-ring-track" cx="24" cy="24" r="20" fill="none" stroke-width="3"/>
|
<circle id="ring-track" class="timer-ring-track" cx="24" cy="24" r="20" fill="none" stroke-width="3" />
|
||||||
<circle id="ring" class="timer-ring-progress" cx="24" cy="24" r="20" fill="none" stroke="#34c759" stroke-width="3"
|
<circle id="ring" class="timer-ring-progress" cx="24" cy="24" r="20" fill="none" stroke="var(--success)"
|
||||||
stroke-linecap="round"
|
stroke-width="3" stroke-linecap="round" stroke-dasharray="125.66" stroke-dashoffset="0" />
|
||||||
stroke-dasharray="125.66" stroke-dashoffset="0"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<i data-lucide="timer" class="timer-icon" width="18" height="18" stroke-width="1.8"></i>
|
<i data-lucide="timer" class="timer-icon" width="18" height="18" stroke-width="1.8"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Big countdown above QR -->
|
<!-- Countdown -->
|
||||||
<p class="text-[32px] font-bold font-mono leading-none text-success text-center mb-4" id="countdown">--:--</p>
|
<p class="text-3xl font-bold font-mono leading-none text-success text-center mb-4" id="countdown">--:--</p>
|
||||||
<div class="flex justify-center">
|
<!-- QR code -->
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
<div class="qr-wrapper">
|
<div class="qr-wrapper">
|
||||||
<div id="qrcode" class="w-44 h-44"></div>
|
<div id="qrcode" class="w-44 h-44"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Address row (edge-to-edge inside card) -->
|
||||||
<!-- hidden inline countdown (used by JS, not shown) -->
|
<div class="h-px bg-border/50 -mx-5"></div>
|
||||||
<span id="countdown-inline" class="sr-only"></span>
|
<div id="copy-addr-box" class="row cursor-pointer select-none -mx-5">
|
||||||
|
|
||||||
<!-- Copy rows -->
|
|
||||||
<div class="glass-card w-full overflow-hidden mb-3">
|
|
||||||
<!-- Amount row -->
|
|
||||||
<div id="copy-amt-box" class="ios-row cursor-pointer select-none">
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-0.5" data-i18n="amount_to_pay">Amount to pay</p>
|
|
||||||
<p class="text-[15px] font-semibold text-card-foreground" id="field-amount">--</p>
|
|
||||||
</div>
|
|
||||||
<span class="ios-icon-btn shrink-0 pointer-events-none" id="btn-copy-amt2">
|
|
||||||
<i data-lucide="copy" width="14" height="14" stroke-width="2"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="h-px bg-border/50 mx-4"></div>
|
|
||||||
<!-- Address row -->
|
|
||||||
<div id="copy-addr-box" class="ios-row cursor-pointer select-none">
|
|
||||||
<div class="flex-1 min-w-0 pr-3">
|
<div class="flex-1 min-w-0 pr-3">
|
||||||
<p class="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-0.5" data-i18n="payment_address">Payment address</p>
|
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-0.5"
|
||||||
<p class="text-[13px] font-medium text-card-foreground break-all leading-relaxed" id="field-address">--</p>
|
data-i18n="payment_address">Payment address</p>
|
||||||
|
<p class="text-sm font-medium text-card-foreground break-all leading-relaxed" id="field-address">--</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ios-icon-btn shrink-0 pointer-events-none self-start mt-0.5" id="btn-copy-addr">
|
<span class="icon-btn shrink-0 pointer-events-none self-start mt-0.5" id="btn-copy-addr">
|
||||||
<i data-lucide="copy" width="14" height="14" stroke-width="2"></i>
|
<i data-lucide="copy" width="14" height="14" stroke-width="2"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span id="countdown-inline" class="sr-only"></span>
|
||||||
<!-- CTA -->
|
<!-- Connect Wallet (above I have transferred) -->
|
||||||
<button class="ios-btn-primary w-full mb-3" id="btn-transferred" onclick="handleTransfer()">
|
<button class="btn-secondary w-full mb-3" id="btn-connect-wallet" onclick="connectWallet()">
|
||||||
|
<i data-lucide="wallet" width="16" height="16" stroke-width="1.8" class="mr-1.5 shrink-0"></i>
|
||||||
|
<span data-i18n="connect_wallet">Connect Wallet to Pay</span>
|
||||||
|
</button>
|
||||||
|
<!-- I have transferred -->
|
||||||
|
<button class="btn-primary w-full mb-3" id="btn-transferred" onclick="handleTransfer()">
|
||||||
<span data-i18n="i_have_transferred">I have transferred</span>
|
<span data-i18n="i_have_transferred">I have transferred</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<div class="flex items-center justify-center gap-1.5 py-1" id="status-row">
|
<div class="flex items-center justify-center gap-1.5 py-1" id="status-row">
|
||||||
<i data-lucide="loader-circle" class="animate-spin text-muted-foreground" width="13" height="13" stroke-width="2.2"></i>
|
<i data-lucide="loader-circle" class="animate-spin text-muted-foreground" width="13" height="13"
|
||||||
<span class="text-[12px] text-muted-foreground" id="status-text" data-i18n="checking_blockchain">Checking blockchain</span>
|
stroke-width="2.2"></i>
|
||||||
|
<span class="text-xs text-muted-foreground" id="status-text" data-i18n="checking_blockchain">Checking
|
||||||
|
blockchain</span>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- State: Success -->
|
||||||
|
<section id="screen-success"
|
||||||
|
class="card w-full flex flex-col items-center justify-center text-center py-10 px-6 min-h-96 mb-4" role="status"
|
||||||
|
aria-live="polite" style="display:none">
|
||||||
|
<div class="state-icon bg-success/15 mb-6">
|
||||||
|
<i data-lucide="check" width="38" height="38" stroke-width="2.2" stroke="var(--success)"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="text-xl font-bold text-card-foreground mb-2" data-i18n="payment_success">Payment Successful</p>
|
||||||
|
<p class="text-sm text-muted-foreground mb-6" data-i18n="redirecting">Redirecting…</p>
|
||||||
|
<i data-lucide="loader-circle" class="animate-spin text-muted-foreground" width="22" height="22"
|
||||||
|
stroke-width="2"></i>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="status-face status-face-back">
|
<!-- State: Expired -->
|
||||||
<div id="state-panel" class="w-full state-panel-shell">
|
<section id="screen-expired" class="w-full pb-4" role="status" aria-live="polite" style="display:none">
|
||||||
<div class="glass-card w-full flex items-center justify-center min-h-[300px] py-10 px-6">
|
<div class="card w-full flex flex-col items-center justify-center text-center py-10 px-6 mb-4">
|
||||||
|
<div class="state-icon bg-destructive/12 mb-6">
|
||||||
|
<i data-lucide="x" width="38" height="38" stroke-width="2.2" stroke="var(--destructive)"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-bold text-card-foreground mb-2" data-i18n="payment_expired">Payment Expired</p>
|
||||||
|
<p class="text-sm text-muted-foreground" data-i18n="expired_sub">Please initiate a new payment</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn-secondary w-full" onclick="goBack()" data-i18n="back">Back</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Success -->
|
<!-- State: Timeout -->
|
||||||
<div class="animate-state-in state-screen flex flex-col items-center text-center" id="screen-success">
|
<section id="screen-timeout" class="w-full pb-4" role="status" aria-live="polite" style="display:none">
|
||||||
<div class="ios-state-icon bg-[rgba(52,199,89,.15)] mb-6">
|
<div class="card w-full flex flex-col items-center justify-center text-center py-10 px-6 mb-4">
|
||||||
<i data-lucide="check" width="38" height="38" stroke-width="2.2" stroke="#34c759"></i>
|
<div class="state-icon bg-warning/12 mb-6">
|
||||||
|
<i data-lucide="triangle-alert" width="38" height="38" stroke-width="2.2" stroke="var(--warning)"></i>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[20px] font-bold text-card-foreground mb-2" data-i18n="payment_success">Payment Successful</p>
|
<p class="text-xl font-bold text-card-foreground mb-2" data-i18n="network_timeout">Connection Timeout</p>
|
||||||
<p class="text-[14px] text-muted-foreground mb-6" data-i18n="redirecting">Redirecting…</p>
|
<p class="text-sm text-muted-foreground" data-i18n="timeout_sub">Unable to connect to the payment server</p>
|
||||||
<i data-lucide="loader-circle" class="animate-spin text-muted-foreground" width="22" height="22" stroke-width="2"></i>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Expired -->
|
|
||||||
<div class="animate-state-in state-screen flex flex-col items-center text-center" id="screen-expired">
|
|
||||||
<div class="ios-state-icon bg-[rgba(255,59,48,.12)] mb-6">
|
|
||||||
<i data-lucide="x" width="38" height="38" stroke-width="2.2" stroke="#ff3b30"></i>
|
|
||||||
</div>
|
|
||||||
<p class="text-[20px] font-bold text-card-foreground mb-2" data-i18n="payment_expired">Payment Expired</p>
|
|
||||||
<p class="text-[14px] text-muted-foreground mb-7" data-i18n="expired_sub">Please initiate a new payment</p>
|
|
||||||
<button class="ios-btn-secondary" onclick="goBack()" data-i18n="back">Back</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Timeout -->
|
|
||||||
<div class="animate-state-in state-screen flex flex-col items-center text-center" id="screen-timeout">
|
|
||||||
<div class="ios-state-icon bg-[rgba(255,149,0,.12)] mb-6">
|
|
||||||
<i data-lucide="triangle-alert" width="38" height="38" stroke-width="2.2" stroke="#ff9500"></i>
|
|
||||||
</div>
|
|
||||||
<p class="text-[20px] font-bold text-card-foreground mb-2" data-i18n="network_timeout">Connection Timeout</p>
|
|
||||||
<p class="text-[14px] text-muted-foreground mb-7" data-i18n="timeout_sub">Unable to connect to the payment server</p>
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button class="ios-btn-secondary" onclick="goBack()" data-i18n="back">Back</button>
|
<button class="btn-secondary flex-1" onclick="goBack()" data-i18n="back">Back</button>
|
||||||
<button class="ios-btn-primary px-7" onclick="retryPolling()" data-i18n="retry">Retry</button>
|
<button class="btn-primary flex-1" onclick="retryPolling()" data-i18n="retry">Retry</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Not Found -->
|
<!-- State: Not Found -->
|
||||||
<div class="animate-state-in state-screen flex flex-col items-center text-center" id="screen-not-found">
|
<section id="screen-not-found"
|
||||||
<div class="ios-state-icon bg-[rgba(142,142,147,.12)] mb-6">
|
class="card w-full flex flex-col items-center justify-center text-center py-10 px-6 min-h-96 mb-4" role="status"
|
||||||
<i data-lucide="file-x" width="38" height="38" stroke-width="2" stroke="#8e8e93"></i>
|
aria-live="polite" style="display:none">
|
||||||
</div>
|
<div class="state-icon bg-muted-foreground/12 mb-6">
|
||||||
<p class="text-[20px] font-bold text-card-foreground mb-2" data-i18n="order_not_found">Order Not Found</p>
|
<i data-lucide="file-x" width="38" height="38" stroke-width="2" stroke="var(--muted-foreground)"></i>
|
||||||
<p class="text-[14px] text-muted-foreground" data-i18n="not_found_sub">The order does not exist or has already expired</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-xl font-bold text-card-foreground mb-2" data-i18n="order_not_found">Order Not Found</p>
|
||||||
|
<p class="text-sm text-muted-foreground" data-i18n="not_found_sub">The order does not exist or has already
|
||||||
|
expired
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
</div><!-- /panel viewport -->
|
||||||
</div>
|
|
||||||
</div>
|
</main>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="flex items-center justify-center gap-2.5 mt-6 text-[12px] text-muted-foreground select-none">
|
<footer class="flex items-center justify-center gap-2.5 pb-6 text-xs text-muted-foreground select-none relative z-10">
|
||||||
<span class="flex items-center gap-1.5">
|
<span class="flex items-center gap-1.5">
|
||||||
Powered by
|
Powered by
|
||||||
<a href="https://www.gmwallet.app" target="_blank" rel="noopener" class="flex items-center gap-1 font-semibold text-card-foreground hover:opacity-70 transition-opacity">
|
<a href="https://www.gmwallet.app" target="_blank" rel="noopener"
|
||||||
<img src="https://www.gmwallet.app/favicon.png" alt="GM Wallet" class="h-3.5 w-3.5 rounded-[3px]" />
|
class="flex items-center gap-1 font-semibold text-card-foreground hover:opacity-70 transition-opacity">
|
||||||
|
<img src="https://www.gmwallet.app/favicon.png" alt="GM Wallet" class="h-3.5 w-3.5 rounded-xs" />
|
||||||
GM Wallet
|
GM Wallet
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="opacity-30">|</span>
|
<span class="opacity-30">|</span>
|
||||||
<a href="https://github.com/GMwalletApp/epusdt" target="_blank" rel="noopener" class="flex items-center gap-1 hover:opacity-70 transition-opacity">
|
<a href="https://github.com/GMwalletApp" target="_blank" rel="noopener"
|
||||||
|
class="flex items-center gap-1 hover:opacity-70 transition-opacity">
|
||||||
Open source on <span class="font-semibold text-card-foreground">GitHub</span>
|
Open source on <span class="font-semibold text-card-foreground">GitHub</span>
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</div>
|
<!-- Toast -->
|
||||||
|
<div id="toast">Copied</div>
|
||||||
|
|
||||||
<!-- Toast -->
|
<!-- Order data injected by server (Go template) -->
|
||||||
<div id="toast">Copied</div>
|
<script>
|
||||||
|
|
||||||
<!-- Order data injected by server (Go template) -->
|
|
||||||
<script>
|
|
||||||
var ORDER = {
|
var ORDER = {
|
||||||
tradeId: "{{.TradeId}}",
|
tradeId: "{{.TradeId}}",
|
||||||
amount: "{{.Amount}}",
|
amount: "{{.Amount}}",
|
||||||
@@ -263,9 +536,12 @@
|
|||||||
receiveAddress: "{{.ReceiveAddress}}",
|
receiveAddress: "{{.ReceiveAddress}}",
|
||||||
expirationTime: "{{.ExpirationTime}}",
|
expirationTime: "{{.ExpirationTime}}",
|
||||||
redirectUrl: "{{.RedirectUrl}}",
|
redirectUrl: "{{.RedirectUrl}}",
|
||||||
createdAt: "{{.CreatedAt}}"
|
createdAt: "{{.CreatedAt}}",
|
||||||
|
is_selected: "{{.IsSelected}}"
|
||||||
};
|
};
|
||||||
</script>
|
var PAYMENT_OPTIONS = null;
|
||||||
<script defer src="/static/payment.js"></script>
|
</script>
|
||||||
|
<script defer src="/static/payment.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,395 +0,0 @@
|
|||||||
/* =============================================
|
|
||||||
iOS 26 Liquid Glass Design System
|
|
||||||
============================================= */
|
|
||||||
|
|
||||||
/* ---------- CSS Variables ---------- */
|
|
||||||
:root, [data-theme="light"] {
|
|
||||||
--background: #f2f2f7;
|
|
||||||
--foreground: #1c1c1e;
|
|
||||||
--card: rgba(255,255,255,0.72);
|
|
||||||
--card-foreground: #1c1c1e;
|
|
||||||
--popover: rgba(255,255,255,0.88);
|
|
||||||
--popover-foreground:#1c1c1e;
|
|
||||||
--primary: #ffffff;
|
|
||||||
--primary-foreground:#1c1c1e;
|
|
||||||
--primary-hover: #e5e5ea;
|
|
||||||
--secondary: rgba(120,120,128,0.12);
|
|
||||||
--muted: rgba(120,120,128,0.08);
|
|
||||||
--muted-foreground: #8e8e93;
|
|
||||||
--accent: rgba(255,255,255,0.12);
|
|
||||||
--accent-foreground: #1c1c1e;
|
|
||||||
--border: rgba(60,60,67,0.12);
|
|
||||||
--border-hover: rgba(60,60,67,0.22);
|
|
||||||
--success: #34c759;
|
|
||||||
--warning: #ff9500;
|
|
||||||
--destructive: #ff3b30;
|
|
||||||
|
|
||||||
--glass-bg: rgba(255,255,255,0.65);
|
|
||||||
--glass-border: rgba(255,255,255,0.8);
|
|
||||||
--glass-shadow: 0 8px 40px rgba(0,0,0,.10), 0 1.5px 0 rgba(255,255,255,.9) inset;
|
|
||||||
--card-shadow: 0 4px 24px rgba(0,0,0,.08);
|
|
||||||
--popover-shadow: 0 12px 40px rgba(0,0,0,.14);
|
|
||||||
|
|
||||||
--blob-1: rgba(200,200,200,0.18);
|
|
||||||
--blob-2: rgba(52,199,89,0.14);
|
|
||||||
--blob-3: rgba(255,149,0,0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--background: #000000;
|
|
||||||
--foreground: #f5f5f7;
|
|
||||||
--card: rgba(28,28,30,0.82);
|
|
||||||
--card-foreground: #f5f5f7;
|
|
||||||
--popover: rgba(44,44,46,0.92);
|
|
||||||
--popover-foreground:#e5e5ea;
|
|
||||||
--primary: #ffffff;
|
|
||||||
--primary-foreground:#1c1c1e;
|
|
||||||
--primary-hover: #e5e5ea;
|
|
||||||
--secondary: rgba(120,120,128,0.18);
|
|
||||||
--muted: rgba(120,120,128,0.12);
|
|
||||||
--muted-foreground: #636366;
|
|
||||||
--accent: rgba(255,255,255,0.15);
|
|
||||||
--accent-foreground: #ffffff;
|
|
||||||
--border: rgba(255,255,255,0.1);
|
|
||||||
--border-hover: rgba(255,255,255,0.18);
|
|
||||||
--success: #30d158;
|
|
||||||
--warning: #ff9f0a;
|
|
||||||
--destructive: #ff453a;
|
|
||||||
|
|
||||||
--glass-bg: rgba(28,28,30,0.75);
|
|
||||||
--glass-border: rgba(255,255,255,0.10);
|
|
||||||
--glass-shadow: 0 8px 40px rgba(0,0,0,.55), 0 1px 0 rgba(255,255,255,.06) inset;
|
|
||||||
--card-shadow: 0 4px 32px rgba(0,0,0,.55);
|
|
||||||
--popover-shadow: 0 12px 40px rgba(0,0,0,.5);
|
|
||||||
|
|
||||||
--blob-1: rgba(255,255,255,0.14);
|
|
||||||
--blob-2: rgba(48,209,88,0.10);
|
|
||||||
--blob-3: rgba(255,159,10,0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Ambient blobs ---------- */
|
|
||||||
.blob {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: blur(80px);
|
|
||||||
animation: blob-drift 18s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
.blob-1 {
|
|
||||||
width: 420px; height: 420px;
|
|
||||||
background: var(--blob-1);
|
|
||||||
top: -120px; left: -100px;
|
|
||||||
animation-delay: 0s;
|
|
||||||
}
|
|
||||||
.blob-2 {
|
|
||||||
width: 340px; height: 340px;
|
|
||||||
background: var(--blob-2);
|
|
||||||
bottom: -80px; right: -60px;
|
|
||||||
animation-delay: -6s;
|
|
||||||
}
|
|
||||||
.blob-3 {
|
|
||||||
width: 260px; height: 260px;
|
|
||||||
background: var(--blob-3);
|
|
||||||
top: 45%; left: 55%;
|
|
||||||
animation-delay: -12s;
|
|
||||||
}
|
|
||||||
@keyframes blob-drift {
|
|
||||||
0% { transform: translate(0,0) scale(1); }
|
|
||||||
50% { transform: translate(30px,-20px) scale(1.06); }
|
|
||||||
100% { transform: translate(-20px,30px) scale(0.96); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Liquid Glass Card ---------- */
|
|
||||||
.glass-card {
|
|
||||||
background: var(--glass-bg);
|
|
||||||
-webkit-backdrop-filter: blur(28px) saturate(1.6);
|
|
||||||
backdrop-filter: blur(28px) saturate(1.6);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
border-radius: 22px;
|
|
||||||
box-shadow: var(--glass-shadow);
|
|
||||||
transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Flippable Status Card ---------- */
|
|
||||||
.status-card {
|
|
||||||
position: relative;
|
|
||||||
perspective: 1600px;
|
|
||||||
transition: height 0.38s cubic-bezier(.22,1,.36,1);
|
|
||||||
}
|
|
||||||
.status-card-inner {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
transition: transform 0.82s cubic-bezier(.22,.9,.24,1);
|
|
||||||
}
|
|
||||||
.status-card.is-flipped .status-card-inner {
|
|
||||||
transform: rotateY(180deg);
|
|
||||||
}
|
|
||||||
.status-face {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
-webkit-backface-visibility: hidden;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
}
|
|
||||||
.status-face-front {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.status-face-back {
|
|
||||||
transform: rotateY(180deg);
|
|
||||||
}
|
|
||||||
.state-panel-shell {
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
.state-screen {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.state-screen.is-active {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Timer ---------- */
|
|
||||||
.timer-ring-track {
|
|
||||||
stroke: var(--border);
|
|
||||||
transition: stroke .3s;
|
|
||||||
}
|
|
||||||
.timer-ring-progress {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
transform-origin: 50% 50%;
|
|
||||||
transition: stroke-dashoffset 1s linear, stroke .5s;
|
|
||||||
}
|
|
||||||
.timer-icon {
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- QR wrapper ---------- */
|
|
||||||
.qr-wrapper {
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 14px;
|
|
||||||
box-shadow: 0 2px 16px rgba(0,0,0,.12), 0 0 0 1px rgba(0,0,0,.04);
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- iOS Pill button ---------- */
|
|
||||||
.ios-pill {
|
|
||||||
background: var(--glass-bg);
|
|
||||||
-webkit-backdrop-filter: blur(16px) saturate(1.4);
|
|
||||||
backdrop-filter: blur(16px) saturate(1.4);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
border-radius: 99px;
|
|
||||||
padding: 6px 13px;
|
|
||||||
color: var(--card-foreground);
|
|
||||||
transition: background 0.15s, opacity 0.15s;
|
|
||||||
}
|
|
||||||
.ios-pill:hover { opacity: 0.82; }
|
|
||||||
.ios-pill:active { opacity: 0.6; }
|
|
||||||
.ios-pill.w-9 { padding: 0; justify-content: center; }
|
|
||||||
|
|
||||||
/* ---------- iOS Rows ---------- */
|
|
||||||
.ios-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 14px 16px;
|
|
||||||
transition: background 0.12s;
|
|
||||||
}
|
|
||||||
.ios-row:active { background: var(--muted); }
|
|
||||||
|
|
||||||
/* ---------- Icon button ---------- */
|
|
||||||
.ios-icon-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 32px; height: 32px;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
background: var(--secondary);
|
|
||||||
transition: background 0.15s, color 0.15s;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.ios-icon-btn:hover { background: var(--accent); color: var(--primary); }
|
|
||||||
|
|
||||||
/* ---------- Primary CTA button ---------- */
|
|
||||||
.ios-btn-primary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
border: none;
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 16px 24px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s, transform 0.1s, opacity 0.15s;
|
|
||||||
box-shadow: 0 4px 18px rgba(0,0,0,0.12), 0 1px 0 rgba(255,255,255,.2) inset;
|
|
||||||
}
|
|
||||||
[data-theme="dark"] .ios-btn-primary {
|
|
||||||
box-shadow: 0 4px 18px rgba(255,255,255,0.18), 0 1px 0 rgba(255,255,255,.12) inset;
|
|
||||||
}
|
|
||||||
.ios-btn-primary:hover { background: var(--primary-hover); }
|
|
||||||
.ios-btn-primary:active { transform: scale(0.97); opacity: 0.9; }
|
|
||||||
.ios-btn-primary:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
|
|
||||||
|
|
||||||
/* ---------- Secondary button ---------- */
|
|
||||||
.ios-btn-secondary {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--secondary);
|
|
||||||
color: var(--card-foreground);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 13px;
|
|
||||||
padding: 11px 28px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s, opacity 0.15s;
|
|
||||||
}
|
|
||||||
.ios-btn-secondary:hover { background: var(--accent); }
|
|
||||||
.ios-btn-secondary:active { opacity: 0.7; }
|
|
||||||
|
|
||||||
/* ---------- State icon circle ---------- */
|
|
||||||
.ios-state-icon {
|
|
||||||
width: 76px; height: 76px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Context Menu ---------- */
|
|
||||||
.ios-menu {
|
|
||||||
background: var(--popover);
|
|
||||||
-webkit-backdrop-filter: blur(30px) saturate(1.8);
|
|
||||||
backdrop-filter: blur(30px) saturate(1.8);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: var(--popover-shadow);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.ios-menu.is-open {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
.ios-menu-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 11px 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--card-foreground);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.1s;
|
|
||||||
}
|
|
||||||
.ios-menu-item:hover { background: var(--accent); }
|
|
||||||
.ios-menu-item.is-selected { color: var(--primary); font-weight: 600; }
|
|
||||||
.ios-menu-item + .ios-menu-item { border-top: 1px solid var(--border); }
|
|
||||||
|
|
||||||
/* ---------- Select states ---------- */
|
|
||||||
.select-trigger.is-open { opacity: 0.8; }
|
|
||||||
.select-trigger.is-open .select-chevron { transform: rotate(180deg); }
|
|
||||||
|
|
||||||
/* ---------- Toast ---------- */
|
|
||||||
#toast {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 36px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%) translateY(12px);
|
|
||||||
background: rgba(28,28,30,0.88);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.12);
|
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,.3);
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.22s, transform 0.22s;
|
|
||||||
z-index: 9999;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
[data-theme="light"] #toast {
|
|
||||||
background: rgba(44,44,46,0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Animations ---------- */
|
|
||||||
@keyframes state-in {
|
|
||||||
from { opacity: 0; transform: translateY(12px) scale(0.96); }
|
|
||||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- View Transition ---------- */
|
|
||||||
::view-transition-old(root),
|
|
||||||
::view-transition-new(root) {
|
|
||||||
animation: none;
|
|
||||||
mix-blend-mode: normal;
|
|
||||||
}
|
|
||||||
::view-transition-old(root) { z-index: 1; }
|
|
||||||
::view-transition-new(root) { z-index: 9; }
|
|
||||||
|
|
||||||
/* ---------- Debug toolbar ---------- */
|
|
||||||
#debug-bar {
|
|
||||||
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
|
|
||||||
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
|
||||||
}
|
|
||||||
#debug-toggle {
|
|
||||||
width: 36px; height: 36px; border-radius: 50%; border: none;
|
|
||||||
background: #7c3aed; color: #fff; cursor: pointer;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
box-shadow: 0 2px 12px rgba(124,58,237,.5);
|
|
||||||
transition: background .15s, transform .1s;
|
|
||||||
}
|
|
||||||
#debug-toggle:hover { background: #6d28d9; }
|
|
||||||
#debug-toggle:active { transform: scale(.92); }
|
|
||||||
#debug-panel {
|
|
||||||
display: none; flex-direction: column; gap: 5px;
|
|
||||||
background: rgba(15,15,20,.92);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(124,58,237,.35); border-radius: 14px;
|
|
||||||
padding: 12px; min-width: 150px;
|
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,.5);
|
|
||||||
}
|
|
||||||
#debug-panel.is-open { display: flex; }
|
|
||||||
.debug-label {
|
|
||||||
font-size: 10px; font-weight: 700; color: #a78bfa;
|
|
||||||
letter-spacing: .12em; text-transform: uppercase;
|
|
||||||
margin-bottom: 4px; padding-bottom: 6px;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
|
||||||
}
|
|
||||||
.debug-btn {
|
|
||||||
background: rgba(255,255,255,.07); color: #d1d5db;
|
|
||||||
border: 1px solid rgba(255,255,255,.1); border-radius: 8px;
|
|
||||||
padding: 6px 10px; font-size: 12px; font-weight: 500;
|
|
||||||
cursor: pointer; text-align: left; font-family: inherit;
|
|
||||||
transition: background .12s, color .12s;
|
|
||||||
}
|
|
||||||
.debug-btn:hover { background: rgba(255,255,255,.14); color: #fff; }
|
|
||||||
.debug-btn--reset:hover { border-color: #60a5fa; color: #93c5fd; }
|
|
||||||
.debug-btn--success:hover { border-color: #34d399; color: #6ee7b7; }
|
|
||||||
.debug-btn--expired:hover { border-color: #f87171; color: #fca5a5; }
|
|
||||||
.debug-btn--timeout:hover { border-color: #fb923c; color: #fdba74; }
|
|
||||||
.debug-btn--urgent:hover { border-color: #facc15; color: #fde68a; }
|
|
||||||
.debug-btn--not-found {
|
|
||||||
background: rgba(142,142,147,.15);
|
|
||||||
color: #8e8e93;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.status-card,
|
|
||||||
.status-card-inner,
|
|
||||||
.status-face,
|
|
||||||
.timer-ring-progress {
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+658
-271
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
func Start() {
|
func Start() {
|
||||||
log.Sugar.Info("[task] Starting task scheduler...")
|
log.Sugar.Info("[task] Starting task scheduler...")
|
||||||
|
go StartEthereumWebSocketListener()
|
||||||
|
|
||||||
c := cron.New()
|
c := cron.New()
|
||||||
// trc20钱包监听
|
// trc20钱包监听
|
||||||
_, err := c.AddJob("@every 5s", ListenTrc20Job{})
|
_, err := c.AddJob("@every 5s", ListenTrc20Job{})
|
||||||
@@ -15,6 +17,13 @@ func Start() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Sugar.Info("[task] ListenTrc20Job scheduled successfully (@every 5s)")
|
log.Sugar.Info("[task] ListenTrc20Job scheduled successfully (@every 5s)")
|
||||||
|
// solana钱包监听
|
||||||
|
_, err = c.AddJob("@every 5s", ListenSolJob{})
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[task] Failed to add ListenSolJob: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Sugar.Info("[task] ListenSolJob scheduled successfully (@every 5s)")
|
||||||
c.Start()
|
c.Start()
|
||||||
log.Sugar.Info("[task] Task scheduler started")
|
log.Sugar.Info("[task] Task scheduler started")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/assimon/luuu/model/data"
|
||||||
|
"github.com/assimon/luuu/model/mdb"
|
||||||
|
"github.com/assimon/luuu/model/service"
|
||||||
|
"github.com/assimon/luuu/util/log"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
|
||||||
|
// USDT / USDC 合约地址(ETH 主网)
|
||||||
|
usdtContract = common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")
|
||||||
|
usdcContract = common.HexToAddress("0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
|
||||||
|
|
||||||
|
// Transfer 事件签名
|
||||||
|
transferEventHash = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ethRecipientSnapshot struct {
|
||||||
|
addrs map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ethWatchedRecipients atomic.Pointer[ethRecipientSnapshot]
|
||||||
|
|
||||||
|
func StartEthereumWebSocketListener() {
|
||||||
|
wallets, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkEthereum)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Fatalf("Failed to get wallet addresses: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
StoreEthRecipientsFromWallets(wallets)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
w, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkEthereum)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[ETH-WS] refresh wallet addresses: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
StoreEthRecipientsFromWallets(w)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wsURL := "wss://ethereum.publicnode.com"
|
||||||
|
|
||||||
|
client, err := ethclient.Dial(wsURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Fatal("连接失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志通道
|
||||||
|
logsCh := make(chan types.Log)
|
||||||
|
|
||||||
|
// 订阅条件
|
||||||
|
query := ethereum.FilterQuery{
|
||||||
|
Addresses: []common.Address{
|
||||||
|
usdtContract,
|
||||||
|
usdcContract,
|
||||||
|
},
|
||||||
|
Topics: [][]common.Hash{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅日志(核心)
|
||||||
|
sub, err := client.SubscribeFilterLogs(context.Background(), query, logsCh)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Fatal("订阅失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("🚀 开始监听 USDT / USDC 收款...")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-sub.Err():
|
||||||
|
log.Sugar.Fatal("订阅错误:", err)
|
||||||
|
|
||||||
|
case vLog := <-logsCh:
|
||||||
|
|
||||||
|
// topics:
|
||||||
|
// [0] Transfer event
|
||||||
|
// [1] from
|
||||||
|
// [2] to
|
||||||
|
if len(vLog.Topics) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
event := vLog.Topics[0].String()
|
||||||
|
if event != transferEventHash.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// value from data
|
||||||
|
amount := new(big.Int).SetBytes(vLog.Data)
|
||||||
|
|
||||||
|
|
||||||
|
if event != transferEventHash.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
toAddr := common.HexToAddress(vLog.Topics[2].Hex())
|
||||||
|
|
||||||
|
if !isWatchedEthRecipient(toAddr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockTsMs int64
|
||||||
|
header, err := client.HeaderByNumber(context.Background(), big.NewInt(int64(vLog.BlockNumber)))
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[ETH-WS] HeaderByNumber block=%d: %v, using local time", vLog.BlockNumber, err)
|
||||||
|
blockTsMs = time.Now().UnixMilli()
|
||||||
|
} else {
|
||||||
|
blockTsMs = int64(header.Time) * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
service.TryProcessEthereumERC20Transfer(vLog.Address, toAddr, amount, vLog.TxHash.Hex(), blockTsMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StoreEthRecipientsFromWallets(wallets []mdb.WalletAddress) int {
|
||||||
|
m := make(map[string]struct{})
|
||||||
|
for _, w := range wallets {
|
||||||
|
a := strings.TrimSpace(w.Address)
|
||||||
|
if !common.IsHexAddress(a) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m[strings.ToLower(common.HexToAddress(a).Hex())] = struct{}{}
|
||||||
|
}
|
||||||
|
ethWatchedRecipients.Store(ðRecipientSnapshot{addrs: m})
|
||||||
|
return len(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWatchedEthRecipient(to common.Address) bool {
|
||||||
|
snap := ethWatchedRecipients.Load()
|
||||||
|
if snap == nil || len(snap.addrs) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := snap.addrs[strings.ToLower(to.Hex())]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAmount(amount *big.Int, decimals int) string {
|
||||||
|
f := new(big.Float).SetInt(amount)
|
||||||
|
divisor := new(big.Float).SetFloat64(float64Pow(10, decimals))
|
||||||
|
result := new(big.Float).Quo(f, divisor)
|
||||||
|
|
||||||
|
return result.Text('f', 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func float64Pow(a, b int) float64 {
|
||||||
|
result := 1.0
|
||||||
|
for i := 0; i < b; i++ {
|
||||||
|
result *= float64(a)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/assimon/luuu/model/data"
|
||||||
|
"github.com/assimon/luuu/model/mdb"
|
||||||
|
"github.com/assimon/luuu/model/service"
|
||||||
|
"github.com/assimon/luuu/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListenSolJob struct{}
|
||||||
|
|
||||||
|
var gListenSolJobLock sync.Mutex
|
||||||
|
|
||||||
|
func (r ListenSolJob) Run() {
|
||||||
|
gListenSolJobLock.Lock()
|
||||||
|
defer gListenSolJobLock.Unlock()
|
||||||
|
log.Sugar.Debug("[ListenSolJob] Job triggered")
|
||||||
|
walletAddress, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkSolana)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[ListenSolJob] Failed to get wallet addresses: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(walletAddress) <= 0 {
|
||||||
|
log.Sugar.Debug("[ListenSolJob] No available wallet addresses")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Sugar.Infof("[ListenSolJob] Found %d wallet addresses to monitor", len(walletAddress))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, address := range walletAddress {
|
||||||
|
log.Sugar.Infof("[ListenSolJob] Listening to address: %s", address.Address)
|
||||||
|
wg.Add(1)
|
||||||
|
go service.SolCallBack(address.Address, &wg)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
log.Sugar.Debug("[ListenSolJob] Job completed")
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/assimon/luuu/model/data"
|
||||||
|
"github.com/assimon/luuu/model/mdb"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/assimon/luuu/model/service"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/assimon/luuu/util/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListenTrc20Job struct {
|
type ListenTrc20Job struct{}
|
||||||
}
|
|
||||||
|
|
||||||
var gListenTrc20JobLock sync.Mutex
|
var gListenTrc20JobLock sync.Mutex
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ func (r ListenTrc20Job) Run() {
|
|||||||
gListenTrc20JobLock.Lock()
|
gListenTrc20JobLock.Lock()
|
||||||
defer gListenTrc20JobLock.Unlock()
|
defer gListenTrc20JobLock.Unlock()
|
||||||
log.Sugar.Debug("[ListenTrc20Job] Job triggered")
|
log.Sugar.Debug("[ListenTrc20Job] Job triggered")
|
||||||
walletAddress, err := data.GetAvailableWalletAddress()
|
walletAddress, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkTron)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Sugar.Errorf("[ListenTrc20Job] Failed to get wallet addresses: %v", err)
|
log.Sugar.Errorf("[ListenTrc20Job] Failed to get wallet addresses: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
+24
-8
@@ -2,6 +2,7 @@ package telegram
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ReplayAddWallet = "请发给我一个合法的钱包地址"
|
ReplayAddWallet = "请发给我 TRON(T 开头)、以太坊主网(0x 开头)或 Solana 收款地址"
|
||||||
pendingWalletAddressTTL = 5 * time.Minute
|
pendingWalletAddressTTL = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,13 +46,19 @@ func OnTextMessageHandle(c tb.Context) error {
|
|||||||
defer bots.Delete(msg.ReplyTo)
|
defer bots.Delete(msg.ReplyTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
msgText := msg.Text
|
msgText := strings.TrimSpace(msg.Text)
|
||||||
if !isValidTronAddress(msgText) {
|
var err error
|
||||||
_ = c.Send(fmt.Sprintf("钱包 [%s] 添加失败:不是合法的 TRON 地址", msgText))
|
switch {
|
||||||
|
case isValidTronAddress(msgText):
|
||||||
|
_, err = data.AddWalletAddress(msgText)
|
||||||
|
case isValidEthereumAddress(msgText):
|
||||||
|
_, err = data.AddWalletAddressWithNetwork(mdb.NetworkEthereum, strings.ToLower(msgText))
|
||||||
|
case isValidSolanaAddress(msgText):
|
||||||
|
_, err = data.AddWalletAddressWithNetwork(mdb.NetworkSolana, msgText)
|
||||||
|
default:
|
||||||
|
_ = c.Send(fmt.Sprintf("钱包 [%s] 添加失败:不是合法的 TRON、以太坊或 Solana 地址", msgText))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := data.AddWalletAddress(msgText)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Send(err.Error())
|
return c.Send(err.Error())
|
||||||
}
|
}
|
||||||
@@ -73,10 +80,14 @@ func WalletList(c tb.Context) error {
|
|||||||
if wallet.Status == mdb.TokenStatusDisable {
|
if wallet.Status == mdb.TokenStatusDisable {
|
||||||
status = "已禁用🚫"
|
status = "已禁用🚫"
|
||||||
}
|
}
|
||||||
|
net := wallet.Network
|
||||||
|
if net == "" {
|
||||||
|
net = mdb.NetworkTron
|
||||||
|
}
|
||||||
|
|
||||||
btnInfo := tb.InlineButton{
|
btnInfo := tb.InlineButton{
|
||||||
Unique: wallet.Address,
|
Unique: wallet.Address,
|
||||||
Text: fmt.Sprintf("%s [%s]", wallet.Address, status),
|
Text: fmt.Sprintf("[%s] %s [%s]", net, wallet.Address, status),
|
||||||
Data: strutil.MustString(wallet.ID),
|
Data: strutil.MustString(wallet.ID),
|
||||||
}
|
}
|
||||||
bots.Handle(&btnInfo, WalletInfo)
|
bots.Handle(&btnInfo, WalletInfo)
|
||||||
@@ -131,7 +142,12 @@ func WalletInfo(c tb.Context) error {
|
|||||||
bots.Handle(&delBtn, DelWallet)
|
bots.Handle(&delBtn, DelWallet)
|
||||||
bots.Handle(&backBtn, WalletList)
|
bots.Handle(&backBtn, WalletList)
|
||||||
|
|
||||||
return c.EditOrReply(tokenInfo.Address, &tb.ReplyMarkup{InlineKeyboard: [][]tb.InlineButton{
|
net := tokenInfo.Network
|
||||||
|
if net == "" {
|
||||||
|
net = mdb.NetworkTron
|
||||||
|
}
|
||||||
|
detail := fmt.Sprintf("网络:%s\n地址:%s", net, tokenInfo.Address)
|
||||||
|
return c.EditOrReply(detail, &tb.ReplyMarkup{InlineKeyboard: [][]tb.InlineButton{
|
||||||
{
|
{
|
||||||
enableBtn,
|
enableBtn,
|
||||||
disableBtn,
|
disableBtn,
|
||||||
|
|||||||
@@ -2,10 +2,23 @@ package telegram
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/btcsuite/btcutil/base58"
|
"github.com/btcsuite/btcutil/base58"
|
||||||
|
"github.com/gagliardetto/solana-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isValidEthereumAddress 校验 0x + 20 字节十六进制(主网收款)。
|
||||||
|
func isValidEthereumAddress(addr string) bool {
|
||||||
|
addr = strings.TrimSpace(addr)
|
||||||
|
if len(addr) != 42 || !strings.HasPrefix(addr, "0x") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := hex.DecodeString(addr[2:])
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
// isValidTronAddress 校验 Tron Base58Check 地址是否合法
|
// isValidTronAddress 校验 Tron Base58Check 地址是否合法
|
||||||
func isValidTronAddress(addr string) bool {
|
func isValidTronAddress(addr string) bool {
|
||||||
// 基本过滤
|
// 基本过滤
|
||||||
@@ -32,3 +45,13 @@ func isValidTronAddress(addr string) bool {
|
|||||||
|
|
||||||
return string(checksum) == string(hash2[:4])
|
return string(checksum) == string(hash2[:4])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isValidSolanaAddress 校验 Solana Base58 地址是否合法(32 字节公钥)。
|
||||||
|
func isValidSolanaAddress(addr string) bool {
|
||||||
|
addr = strings.TrimSpace(addr)
|
||||||
|
if len(addr) < 32 || len(addr) > 44 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := solana.PublicKeyFromBase58(addr)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ var Errno = map[int]string{
|
|||||||
10008: "order does not exist",
|
10008: "order does not exist",
|
||||||
10009: "failed to parse request params",
|
10009: "failed to parse request params",
|
||||||
10010: "order status already changed",
|
10010: "order status already changed",
|
||||||
|
10011: "exceeded maximum sub-order limit",
|
||||||
|
10012: "cannot switch network on a sub-order",
|
||||||
|
10013: "order is not awaiting payment",
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -28,6 +31,9 @@ var (
|
|||||||
OrderNotExists = Err(10008)
|
OrderNotExists = Err(10008)
|
||||||
ParamsMarshalErr = Err(10009)
|
ParamsMarshalErr = Err(10009)
|
||||||
OrderStatusConflict = Err(10010)
|
OrderStatusConflict = Err(10010)
|
||||||
|
SubOrderLimitExceeded = Err(10011)
|
||||||
|
CannotSwitchSubOrder = Err(10012)
|
||||||
|
OrderNotWaitPay = Err(10013)
|
||||||
)
|
)
|
||||||
|
|
||||||
type RspError struct {
|
type RspError struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user