mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 02:06:16 +00:00
Merge branch 'dev' into dev-runtime
This commit is contained in:
@@ -57,3 +57,5 @@ api_auth_token=
|
||||
order_expiration_time=10
|
||||
order_notice_max_retry=0
|
||||
forced_usdt_rate=
|
||||
api_rate_url=
|
||||
tron_grid_api_key=
|
||||
|
||||
+91
-24
@@ -2,27 +2,33 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/assimon/luuu/util/http_client"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var (
|
||||
AppDebug bool
|
||||
MysqlDns string
|
||||
RuntimePath string
|
||||
LogSavePath string
|
||||
StaticPath string
|
||||
TgBotToken string
|
||||
TgProxy string
|
||||
TgManage int64
|
||||
UsdtRate float64
|
||||
BuildVersion = "0.0.0-dev"
|
||||
BuildCommit = "none"
|
||||
BuildDate = "unknown"
|
||||
AppDebug bool
|
||||
MysqlDns string
|
||||
RuntimePath string
|
||||
LogSavePath string
|
||||
StaticPath string
|
||||
TgBotToken string
|
||||
TgProxy string
|
||||
TgManage int64
|
||||
UsdtRate float64
|
||||
RateApiUrl string
|
||||
TRON_GRID_API_KEY string
|
||||
BuildVersion = "0.0.0-dev"
|
||||
BuildCommit = "none"
|
||||
BuildDate = "unknown"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -38,27 +44,21 @@ func Init() {
|
||||
}
|
||||
AppDebug = viper.GetBool("app_debug")
|
||||
StaticPath = viper.GetString("static_path")
|
||||
RuntimePath = fmt.Sprintf(
|
||||
"%s%s",
|
||||
gwd,
|
||||
viper.GetString("runtime_root_path"))
|
||||
LogSavePath = fmt.Sprintf(
|
||||
"%s%s",
|
||||
RuntimePath,
|
||||
viper.GetString("log_save_path"))
|
||||
RuntimePath = fmt.Sprintf("%s%s", gwd, viper.GetString("runtime_root_path"))
|
||||
LogSavePath = fmt.Sprintf("%s%s", RuntimePath, viper.GetString("log_save_path"))
|
||||
mustMkdir(RuntimePath)
|
||||
mustMkdir(LogSavePath)
|
||||
MysqlDns = fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
url.QueryEscape(viper.GetString("mysql_user")),
|
||||
url.QueryEscape(viper.GetString("mysql_passwd")),
|
||||
fmt.Sprintf(
|
||||
"%s:%s",
|
||||
viper.GetString("mysql_host"),
|
||||
viper.GetString("mysql_port")),
|
||||
fmt.Sprintf("%s:%s", viper.GetString("mysql_host"), viper.GetString("mysql_port")),
|
||||
viper.GetString("mysql_database"))
|
||||
TgBotToken = viper.GetString("tg_bot_token")
|
||||
TgProxy = viper.GetString("tg_proxy")
|
||||
TgManage = viper.GetInt64("tg_manage")
|
||||
|
||||
RateApiUrl = GetRateApiUrl()
|
||||
TRON_GRID_API_KEY = viper.GetString("tron_grid_api_key")
|
||||
}
|
||||
|
||||
func mustMkdir(path string) {
|
||||
@@ -95,6 +95,73 @@ func GetApiAuthToken() string {
|
||||
return viper.GetString("api_auth_token")
|
||||
}
|
||||
|
||||
func GetRateApiUrl() string {
|
||||
rateURL := viper.GetString("api_rate_url")
|
||||
if rateURL == "" {
|
||||
rateURL = os.Getenv("API_RATE_URL")
|
||||
}
|
||||
if rateURL == "" {
|
||||
log.Println("api_rate_url is empty")
|
||||
}
|
||||
RateApiUrl = rateURL
|
||||
return rateURL
|
||||
}
|
||||
|
||||
func GetRateForCoin(coin string, base string) float64 {
|
||||
coin = strings.ToLower(strings.TrimSpace(coin))
|
||||
base = strings.ToLower(strings.TrimSpace(base))
|
||||
if coin == "" || base == "" {
|
||||
return 0
|
||||
}
|
||||
if coin == base {
|
||||
return 1
|
||||
}
|
||||
if coin == "usdt" {
|
||||
switch base {
|
||||
case "usd":
|
||||
return 1
|
||||
case "cny":
|
||||
usdtRate := GetUsdtRate()
|
||||
if usdtRate > 0 {
|
||||
return 1 / usdtRate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseURL := RateApiUrl
|
||||
if baseURL == "" {
|
||||
baseURL = GetRateApiUrl()
|
||||
}
|
||||
if baseURL == "" {
|
||||
log.Printf("rate api url is empty")
|
||||
return 0.0
|
||||
}
|
||||
if baseURL[len(baseURL)-1] != '/' {
|
||||
baseURL += "/"
|
||||
}
|
||||
|
||||
client := http_client.GetHttpClient()
|
||||
resp, err := client.R().Get(baseURL + fmt.Sprintf("%s.json", base))
|
||||
if err != nil {
|
||||
log.Printf("call rate api error: %s", err.Error())
|
||||
return 0.0
|
||||
}
|
||||
if resp.StatusCode() < 200 || resp.StatusCode() >= 300 {
|
||||
log.Printf("call rate api unexpected status: %s", resp.Status())
|
||||
return 0.0
|
||||
}
|
||||
|
||||
targetRate := 0.0
|
||||
gjson.GetBytes(resp.Body(), base).ForEach(func(key, value gjson.Result) bool {
|
||||
if key.String() == coin {
|
||||
targetRate = value.Float()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return targetRate
|
||||
}
|
||||
|
||||
func GetUsdtRate() float64 {
|
||||
forcedUsdtRate := viper.GetFloat64("forced_usdt_rate")
|
||||
if forcedUsdtRate > 0 {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/shengdoushi/base58"
|
||||
)
|
||||
|
||||
const AddressLength = 20
|
||||
const PrefixMainnet = 0x41
|
||||
|
||||
// Encode returns the Base58 encoding of input using the Bitcoin alphabet.
|
||||
func Encode(input []byte) string {
|
||||
return base58.Encode(input, base58.BitcoinAlphabet)
|
||||
}
|
||||
|
||||
// EncodeCheck returns the Base58Check encoding of input (Base58 with a 4-byte SHA-256d checksum).
|
||||
func EncodeCheck(input []byte) string {
|
||||
h256h0 := sha256.New()
|
||||
h256h0.Write(input)
|
||||
h0 := h256h0.Sum(nil)
|
||||
|
||||
h256h1 := sha256.New()
|
||||
h256h1.Write(h0)
|
||||
h1 := h256h1.Sum(nil)
|
||||
|
||||
inputCheck := append(append([]byte(nil), input...), h1[:4]...)
|
||||
|
||||
return Encode(inputCheck)
|
||||
}
|
||||
@@ -15,9 +15,11 @@ require (
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shengdoushi/base58 v1.0.0
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/viper v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
go.uber.org/zap v1.17.0
|
||||
gopkg.in/telebot.v3 v3.0.0
|
||||
gorm.io/driver/mysql v1.5.1
|
||||
@@ -58,6 +60,8 @@ require (
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
|
||||
+10
@@ -360,6 +360,8 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs=
|
||||
github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I=
|
||||
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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
@@ -393,6 +395,14 @@ 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/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
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/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
|
||||
@@ -8,5 +8,9 @@ import (
|
||||
)
|
||||
|
||||
func openDB(dsn string, cfg *gorm.Config) (*gorm.DB, error) {
|
||||
return gorm.Open(sqlite.Open(dsn), cfg)
|
||||
db, err := gorm.Open(sqlite.Open(dsn+"?_journal_mode=WAL&_busy_timeout=5000"), cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -8,5 +8,9 @@ import (
|
||||
)
|
||||
|
||||
func openDB(dsn string, cfg *gorm.Config) (*gorm.DB, error) {
|
||||
return gorm.Open(sqlite.Open(dsn), cfg)
|
||||
db, err := gorm.Open(sqlite.Open(dsn+"?_journal_mode=WAL&_busy_timeout=5000"), cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ func RuntimeInit() error {
|
||||
color.Red.Printf("[runtime_db] sqlite connDB err:%s", err.Error())
|
||||
return err
|
||||
}
|
||||
if err = RuntimeDB.Exec("DROP INDEX IF EXISTS transaction_lock_token_amount_uindex").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = RuntimeDB.AutoMigrate(&mdb.TransactionLock{}); err != nil {
|
||||
color.Red.Printf("[runtime_db] sqlite migrate DB(TransactionLock),err=%s\n", err)
|
||||
return err
|
||||
|
||||
@@ -2,6 +2,7 @@ package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/assimon/luuu/model/dao"
|
||||
@@ -19,6 +20,10 @@ func normalizeLockAmount(amount float64) (int64, string) {
|
||||
return value.Shift(2).IntPart(), value.StringFixed(2)
|
||||
}
|
||||
|
||||
func normalizeLockToken(token string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(token))
|
||||
}
|
||||
|
||||
// GetOrderInfoByOrderId fetches an order by merchant order id.
|
||||
func GetOrderInfoByOrderId(orderId string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
@@ -39,9 +44,9 @@ func CreateOrderWithTransaction(tx *gorm.DB, order *mdb.Orders) error {
|
||||
}
|
||||
|
||||
// GetOrderByBlockIdWithTransaction fetches an order by blockchain tx id.
|
||||
func GetOrderByBlockIdWithTransaction(tx *gorm.DB, blockId string) (*mdb.Orders, error) {
|
||||
func GetOrderByBlockIdWithTransaction(tx *gorm.DB, blockID string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := tx.Model(order).Limit(1).Find(order, "block_transaction_id = ?", blockId).Error
|
||||
err := tx.Model(order).Limit(1).Find(order, "block_transaction_id = ?", blockID).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
@@ -94,12 +99,13 @@ func UpdateOrderIsExpirationById(id uint64, expirationCutoff time.Time) (bool, e
|
||||
return result.RowsAffected > 0, result.Error
|
||||
}
|
||||
|
||||
// GetTradeIdByWalletAddressAndAmount resolves the reserved trade id by token and amount.
|
||||
func GetTradeIdByWalletAddressAndAmount(token string, amount float64) (string, error) {
|
||||
// GetTradeIdByWalletAddressAndAmountAndToken resolves the reserved trade id by address, token and amount.
|
||||
func GetTradeIdByWalletAddressAndAmountAndToken(address string, token string, amount float64) (string, error) {
|
||||
scaledAmount, _ := normalizeLockAmount(amount)
|
||||
var lock mdb.TransactionLock
|
||||
err := dao.RuntimeDB.Model(&mdb.TransactionLock{}).
|
||||
Where("token = ?", token).
|
||||
Where("address = ?", address).
|
||||
Where("token = ?", normalizeLockToken(token)).
|
||||
Where("amount_scaled = ?", scaledAmount).
|
||||
Where("expires_at > ?", time.Now()).
|
||||
Limit(1).
|
||||
@@ -113,26 +119,29 @@ func GetTradeIdByWalletAddressAndAmount(token string, amount float64) (string, e
|
||||
return lock.TradeId, nil
|
||||
}
|
||||
|
||||
// LockTransaction reserves a token+amount pair in sqlite until expiration.
|
||||
func LockTransaction(token, tradeId string, amount float64, expirationTime time.Duration) error {
|
||||
// LockTransaction reserves an address+token+amount pair in sqlite until expiration.
|
||||
func LockTransaction(address, token, tradeID string, amount float64, expirationTime time.Duration) error {
|
||||
scaledAmount, amountText := normalizeLockAmount(amount)
|
||||
normalizedToken := normalizeLockToken(token)
|
||||
now := time.Now()
|
||||
lock := &mdb.TransactionLock{
|
||||
Token: token,
|
||||
Address: address,
|
||||
Token: normalizedToken,
|
||||
AmountScaled: scaledAmount,
|
||||
AmountText: amountText,
|
||||
TradeId: tradeId,
|
||||
TradeId: tradeID,
|
||||
ExpiresAt: now.Add(expirationTime),
|
||||
}
|
||||
|
||||
return dao.RuntimeDB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("token = ?", token).
|
||||
if err := tx.Where("address = ?", address).
|
||||
Where("token = ?", normalizedToken).
|
||||
Where("amount_scaled = ?", scaledAmount).
|
||||
Where("expires_at <= ?", now).
|
||||
Delete(&mdb.TransactionLock{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("trade_id = ?", tradeId).Delete(&mdb.TransactionLock{}).Error; err != nil {
|
||||
if err := tx.Where("trade_id = ?", tradeID).Delete(&mdb.TransactionLock{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,14 +156,18 @@ func LockTransaction(token, tradeId string, amount float64, expirationTime time.
|
||||
})
|
||||
}
|
||||
|
||||
// UnLockTransaction releases the reservation for token+amount.
|
||||
func UnLockTransaction(token string, amount float64) error {
|
||||
// UnLockTransaction releases the reservation for address+token+amount.
|
||||
func UnLockTransaction(address string, token string, amount float64) error {
|
||||
scaledAmount, _ := normalizeLockAmount(amount)
|
||||
return dao.RuntimeDB.Where("token = ?", token).Where("amount_scaled = ?", scaledAmount).Delete(&mdb.TransactionLock{}).Error
|
||||
return dao.RuntimeDB.
|
||||
Where("address = ?", address).
|
||||
Where("token = ?", normalizeLockToken(token)).
|
||||
Where("amount_scaled = ?", scaledAmount).
|
||||
Delete(&mdb.TransactionLock{}).Error
|
||||
}
|
||||
|
||||
func UnLockTransactionByTradeId(tradeId string) error {
|
||||
return dao.RuntimeDB.Where("trade_id = ?", tradeId).Delete(&mdb.TransactionLock{}).Error
|
||||
func UnLockTransactionByTradeId(tradeID string) error {
|
||||
return dao.RuntimeDB.Where("trade_id = ?", tradeID).Delete(&mdb.TransactionLock{}).Error
|
||||
}
|
||||
|
||||
func CleanupExpiredTransactionLocks() error {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
// AddWalletAddress 创建钱包
|
||||
func AddWalletAddress(token string) (*mdb.WalletAddress, error) {
|
||||
exist, err := GetWalletAddressByToken(token)
|
||||
func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
|
||||
exist, err := GetWalletAddressByToken(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -16,17 +16,17 @@ func AddWalletAddress(token string) (*mdb.WalletAddress, error) {
|
||||
return nil, constant.WalletAddressAlreadyExists
|
||||
}
|
||||
walletAddress := &mdb.WalletAddress{
|
||||
Token: token,
|
||||
Status: mdb.TokenStatusEnable,
|
||||
Address: address,
|
||||
Status: mdb.TokenStatusEnable,
|
||||
}
|
||||
err = dao.Mdb.Create(walletAddress).Error
|
||||
return walletAddress, err
|
||||
}
|
||||
|
||||
// GetWalletAddressByToken 通过钱包地址获取token
|
||||
func GetWalletAddressByToken(token string) (*mdb.WalletAddress, error) {
|
||||
// GetWalletAddressByToken 通过钱包地址获取address
|
||||
func GetWalletAddressByToken(address string) (*mdb.WalletAddress, error) {
|
||||
walletAddress := new(mdb.WalletAddress)
|
||||
err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, "token = ?", token).Error
|
||||
err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, "address = ?", address).Error
|
||||
return walletAddress, err
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ type Orders struct {
|
||||
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_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"`
|
||||
Currency string `gorm:"column:currency" json:"currency"`
|
||||
ActualAmount float64 `gorm:"column:actual_amount" json:"actual_amount"`
|
||||
ReceiveAddress string `gorm:"column:receive_address" json:"receive_address"`
|
||||
Token string `gorm:"column:token" json:"token"`
|
||||
Status int `gorm:"column:status;default:1" json:"status"`
|
||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"`
|
||||
|
||||
@@ -4,8 +4,9 @@ import "time"
|
||||
|
||||
type TransactionLock struct {
|
||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Token string `gorm:"column:token;uniqueIndex:transaction_lock_token_amount_uindex,priority:1" json:"token"`
|
||||
AmountScaled int64 `gorm:"column:amount_scaled;uniqueIndex:transaction_lock_token_amount_uindex,priority:2" json:"amount_scaled"`
|
||||
Address string `gorm:"column:address;uniqueIndex:transaction_lock_address_token_amount_uindex,priority:1" json:"address"`
|
||||
Token string `gorm:"column:token;uniqueIndex:transaction_lock_address_token_amount_uindex,priority:2" json:"token"`
|
||||
AmountScaled int64 `gorm:"column:amount_scaled;uniqueIndex:transaction_lock_address_token_amount_uindex,priority:3" json:"amount_scaled"`
|
||||
AmountText string `gorm:"column:amount_text" json:"amount_text"`
|
||||
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"`
|
||||
|
||||
@@ -6,8 +6,8 @@ const (
|
||||
)
|
||||
|
||||
type WalletAddress struct {
|
||||
Token string `gorm:"column:token;uniqueIndex:wallet_address_token_uindex" json:"token"`
|
||||
Status int64 `gorm:"column:status;default:1" json:"status"`
|
||||
Address string `gorm:"column:address;uniqueIndex:wallet_address_address_uindex" json:"address"`
|
||||
Status int64 `gorm:"column:status;default:1" json:"status"`
|
||||
BaseModel
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import "github.com/gookit/validate"
|
||||
// CreateTransactionRequest 创建交易请求
|
||||
type CreateTransactionRequest struct {
|
||||
OrderId string `json:"order_id" validate:"required|maxLen:32"`
|
||||
Currency string `json:"currency" validate:"required"`
|
||||
Token string `json:"token" validate:"required"`
|
||||
Amount float64 `json:"amount" validate:"required|isFloat|gt:0.01"`
|
||||
NotifyUrl string `json:"notify_url" validate:"required"`
|
||||
Signature string `json:"signature" validate:"required"`
|
||||
@@ -14,6 +16,8 @@ type CreateTransactionRequest struct {
|
||||
func (r CreateTransactionRequest) Translates() map[string]string {
|
||||
return validate.MS{
|
||||
"OrderId": "订单号",
|
||||
"Currency": "货币",
|
||||
"Token": "币种",
|
||||
"Amount": "支付金额",
|
||||
"NotifyUrl": "异步回调网址",
|
||||
"Signature": "签名",
|
||||
@@ -22,6 +26,8 @@ func (r CreateTransactionRequest) Translates() map[string]string {
|
||||
|
||||
// OrderProcessingRequest 订单处理
|
||||
type OrderProcessingRequest struct {
|
||||
ReceiveAddress string
|
||||
Currency string
|
||||
Token string
|
||||
Amount float64
|
||||
TradeId string
|
||||
|
||||
@@ -5,8 +5,10 @@ type CreateTransactionResponse struct {
|
||||
TradeId string `json:"trade_id"` // epusdt订单号
|
||||
OrderId string `json:"order_id"` // 客户交易id
|
||||
Amount float64 `json:"amount"` // 订单金额,保留4位小数
|
||||
Currency string `json:"currency"` // 订单货币类型 CNY USD......
|
||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
Token string `json:"token"` // 收款钱包地址
|
||||
ReceiveAddress string `json:"receive_address"` // 收款钱包地址
|
||||
Token string `json:"token"` // 所属币种 TRX USDT......
|
||||
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||
PaymentUrl string `json:"payment_url"` // 收银台地址
|
||||
}
|
||||
@@ -17,7 +19,8 @@ type OrderNotifyResponse struct {
|
||||
OrderId string `json:"order_id"` // 客户交易id
|
||||
Amount float64 `json:"amount"` // 订单金额,保留4位小数
|
||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
Token string `json:"token"` // 收款钱包地址
|
||||
ReceiveAddress string `json:"receive_address"` // 收款钱包地址
|
||||
Token string `json:"token"` // 所属币种 TRX USDT......
|
||||
BlockTransactionId string `json:"block_transaction_id"` // 区块id
|
||||
Signature string `json:"signature"` // 签名
|
||||
Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
||||
|
||||
@@ -3,7 +3,8 @@ package response
|
||||
type CheckoutCounterResponse struct {
|
||||
TradeId string `json:"trade_id"` // epusdt订单号
|
||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
Token string `json:"token"` // 收款钱包地址
|
||||
ReceiveAddress string `json:"receive_address"` // 收款钱包地址
|
||||
Token string `json:"token"` // 所属币种 TRX USDT......
|
||||
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||
RedirectUrl string `json:"redirect_url"`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -35,14 +36,20 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
||||
gCreateTransactionLock.Lock()
|
||||
defer gCreateTransactionLock.Unlock()
|
||||
|
||||
token := strings.ToUpper(strings.TrimSpace(req.Token))
|
||||
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
|
||||
payAmount := math.MustParsePrecFloat64(req.Amount, 2)
|
||||
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(currency))
|
||||
if rate <= 0 {
|
||||
return nil, constant.RateAmountErr
|
||||
}
|
||||
|
||||
decimalPayAmount := decimal.NewFromFloat(payAmount)
|
||||
decimalRate := decimal.NewFromFloat(config.GetUsdtRate())
|
||||
decimalUsdt := decimalPayAmount.Div(decimalRate)
|
||||
decimalTokenAmount := decimalPayAmount.Mul(decimal.NewFromFloat(rate))
|
||||
if decimalPayAmount.Cmp(decimal.NewFromFloat(CnyMinimumPaymentAmount)) == -1 {
|
||||
return nil, constant.PayAmountErr
|
||||
}
|
||||
if decimalUsdt.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
|
||||
if decimalTokenAmount.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
|
||||
return nil, constant.PayAmountErr
|
||||
}
|
||||
|
||||
@@ -62,35 +69,37 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
||||
return nil, constant.NotAvailableWalletAddress
|
||||
}
|
||||
|
||||
tradeId := GenerateCode()
|
||||
amount := math.MustParsePrecFloat64(decimalUsdt.InexactFloat64(), 2)
|
||||
availableToken, availableAmount, err := ReserveAvailableWalletAndAmount(tradeId, amount, walletAddress)
|
||||
tradeID := GenerateCode()
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
||||
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(tradeID, token, amount, walletAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if availableToken == "" {
|
||||
if availableAddress == "" {
|
||||
return nil, constant.NotAvailableAmountErr
|
||||
}
|
||||
|
||||
tx := dao.Mdb.Begin()
|
||||
order := &mdb.Orders{
|
||||
TradeId: tradeId,
|
||||
OrderId: req.OrderId,
|
||||
Amount: req.Amount,
|
||||
ActualAmount: availableAmount,
|
||||
Token: availableToken,
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: req.NotifyUrl,
|
||||
RedirectUrl: req.RedirectUrl,
|
||||
TradeId: tradeID,
|
||||
OrderId: req.OrderId,
|
||||
Amount: req.Amount,
|
||||
Currency: currency,
|
||||
ActualAmount: availableAmount,
|
||||
ReceiveAddress: availableAddress,
|
||||
Token: token,
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: req.NotifyUrl,
|
||||
RedirectUrl: req.RedirectUrl,
|
||||
}
|
||||
if err = data.CreateOrderWithTransaction(tx, order); err != nil {
|
||||
tx.Rollback()
|
||||
_ = data.UnLockTransaction(availableToken, availableAmount)
|
||||
_ = data.UnLockTransactionByTradeId(tradeID)
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
_ = data.UnLockTransaction(availableToken, availableAmount)
|
||||
_ = data.UnLockTransactionByTradeId(tradeID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -99,7 +108,9 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
||||
TradeId: order.TradeId,
|
||||
OrderId: order.OrderId,
|
||||
Amount: order.Amount,
|
||||
Currency: order.Currency,
|
||||
ActualAmount: order.ActualAmount,
|
||||
ReceiveAddress: order.ReceiveAddress,
|
||||
Token: order.Token,
|
||||
ExpirationTime: expirationTime,
|
||||
PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId),
|
||||
@@ -137,22 +148,22 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = data.UnLockTransaction(req.Token, req.Amount); err != nil {
|
||||
if err = data.UnLockTransaction(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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReserveAvailableWalletAndAmount finds and locks a token+amount pair.
|
||||
func ReserveAvailableWalletAndAmount(tradeId string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
||||
availableToken := ""
|
||||
// ReserveAvailableWalletAndAmount finds and locks an address+token+amount pair.
|
||||
func ReserveAvailableWalletAndAmount(tradeID string, token string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
||||
availableAddress := ""
|
||||
availableAmount := amount
|
||||
|
||||
tryLockWalletFunc := func(targetAmount float64) (string, error) {
|
||||
for _, address := range walletAddress {
|
||||
err := data.LockTransaction(address.Token, tradeId, targetAmount, config.GetOrderExpirationTimeDuration())
|
||||
err := data.LockTransaction(address.Address, token, tradeID, targetAmount, config.GetOrderExpirationTimeDuration())
|
||||
if err == nil {
|
||||
return address.Token, nil
|
||||
return address.Address, nil
|
||||
}
|
||||
if errors.Is(err, data.ErrTransactionLocked) {
|
||||
continue
|
||||
@@ -163,20 +174,20 @@ func ReserveAvailableWalletAndAmount(tradeId string, amount float64, walletAddre
|
||||
}
|
||||
|
||||
for i := 0; i < IncrementalMaximumNumber; i++ {
|
||||
token, err := tryLockWalletFunc(availableAmount)
|
||||
address, err := tryLockWalletFunc(availableAmount)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
if token == "" {
|
||||
if address == "" {
|
||||
decimalOldAmount := decimal.NewFromFloat(availableAmount)
|
||||
decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement)
|
||||
availableAmount = decimalOldAmount.Add(decimalIncr).InexactFloat64()
|
||||
continue
|
||||
}
|
||||
availableToken = token
|
||||
availableAddress = address
|
||||
break
|
||||
}
|
||||
return availableToken, availableAmount, nil
|
||||
return availableAddress, availableAmount, nil
|
||||
}
|
||||
|
||||
// GenerateCode creates a unique trade id.
|
||||
|
||||
@@ -13,6 +13,16 @@ import (
|
||||
"github.com/assimon/luuu/util/constant"
|
||||
)
|
||||
|
||||
func newCreateTransactionRequest(orderID string, amount float64) *request.CreateTransactionRequest {
|
||||
return &request.CreateTransactionRequest{
|
||||
OrderId: orderID,
|
||||
Currency: "CNY",
|
||||
Token: "USDT",
|
||||
Amount: amount,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
@@ -21,22 +31,11 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
req1 := &request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
}
|
||||
req2 := &request.CreateTransactionRequest{
|
||||
OrderId: "order_2",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
}
|
||||
|
||||
resp1, err := CreateTransaction(req1)
|
||||
resp1, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
|
||||
if err != nil {
|
||||
t.Fatalf("create first transaction: %v", err)
|
||||
}
|
||||
resp2, err := CreateTransaction(req2)
|
||||
resp2, err := CreateTransaction(newCreateTransactionRequest("order_2", 1))
|
||||
if err != nil {
|
||||
t.Fatalf("create second transaction: %v", err)
|
||||
}
|
||||
@@ -47,11 +46,14 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
|
||||
if got := fmt.Sprintf("%.2f", resp2.ActualAmount); got != "1.01" {
|
||||
t.Fatalf("second actual amount = %s, want 1.01", got)
|
||||
}
|
||||
if resp1.Token != "wallet_1" || resp2.Token != "wallet_1" {
|
||||
t.Fatalf("unexpected wallet tokens: %s, %s", resp1.Token, resp2.Token)
|
||||
if resp1.ReceiveAddress != "wallet_1" || resp2.ReceiveAddress != "wallet_1" {
|
||||
t.Fatalf("unexpected receive addresses: %s, %s", resp1.ReceiveAddress, resp2.ReceiveAddress)
|
||||
}
|
||||
if resp1.Token != "USDT" || resp2.Token != "USDT" {
|
||||
t.Fatalf("unexpected tokens: %s, %s", resp1.Token, resp2.Token)
|
||||
}
|
||||
|
||||
tradeID1, err := data.GetTradeIdByWalletAddressAndAmount(resp1.Token, resp1.ActualAmount)
|
||||
tradeID1, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp1.ReceiveAddress, resp1.Token, resp1.ActualAmount)
|
||||
if err != nil {
|
||||
t.Fatalf("get first runtime lock: %v", err)
|
||||
}
|
||||
@@ -59,7 +61,7 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
|
||||
t.Fatalf("first runtime lock = %s, want %s", tradeID1, resp1.TradeId)
|
||||
}
|
||||
|
||||
tradeID2, err := data.GetTradeIdByWalletAddressAndAmount(resp2.Token, resp2.ActualAmount)
|
||||
tradeID2, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp2.ReceiveAddress, resp2.Token, resp2.ActualAmount)
|
||||
if err != nil {
|
||||
t.Fatalf("get second runtime lock: %v", err)
|
||||
}
|
||||
@@ -76,16 +78,13 @@ func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
resp, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction: %v", err)
|
||||
}
|
||||
|
||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||
ReceiveAddress: resp.ReceiveAddress,
|
||||
Token: resp.Token,
|
||||
TradeId: resp.TradeId,
|
||||
Amount: resp.ActualAmount,
|
||||
@@ -109,7 +108,7 @@ func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) {
|
||||
t.Fatalf("block transaction id = %s, want block_1", order.BlockTransactionId)
|
||||
}
|
||||
|
||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmount(resp.Token, resp.ActualAmount)
|
||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp.ReceiveAddress, resp.Token, resp.ActualAmount)
|
||||
if err != nil {
|
||||
t.Fatalf("get runtime lock after processing: %v", err)
|
||||
}
|
||||
@@ -126,16 +125,13 @@ func TestOrderProcessingRejectsDuplicateBlockForSameOrder(t *testing.T) {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
resp, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction: %v", err)
|
||||
}
|
||||
|
||||
req := &request.OrderProcessingRequest{
|
||||
ReceiveAddress: resp.ReceiveAddress,
|
||||
Token: resp.Token,
|
||||
TradeId: resp.TradeId,
|
||||
Amount: resp.ActualAmount,
|
||||
@@ -170,11 +166,7 @@ func TestOrderProcessingDoesNotReviveExpiredOrder(t *testing.T) {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
resp, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction: %v", err)
|
||||
}
|
||||
@@ -186,6 +178,7 @@ func TestOrderProcessingDoesNotReviveExpiredOrder(t *testing.T) {
|
||||
}
|
||||
|
||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||
ReceiveAddress: resp.ReceiveAddress,
|
||||
Token: resp.Token,
|
||||
TradeId: resp.TradeId,
|
||||
Amount: resp.ActualAmount,
|
||||
@@ -218,19 +211,11 @@ func TestOrderProcessingOnlyOneOrderClaimsABlockTransaction(t *testing.T) {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp1, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
resp1, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
|
||||
if err != nil {
|
||||
t.Fatalf("create first transaction: %v", err)
|
||||
}
|
||||
resp2, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_2",
|
||||
Amount: 2,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
resp2, err := CreateTransaction(newCreateTransactionRequest("order_2", 2))
|
||||
if err != nil {
|
||||
t.Fatalf("create second transaction: %v", err)
|
||||
}
|
||||
@@ -239,24 +224,26 @@ func TestOrderProcessingOnlyOneOrderClaimsABlockTransaction(t *testing.T) {
|
||||
errs := make(chan error, 2)
|
||||
var wg sync.WaitGroup
|
||||
for _, tc := range []struct {
|
||||
address string
|
||||
token string
|
||||
tradeID string
|
||||
amount float64
|
||||
}{
|
||||
{token: resp1.Token, tradeID: resp1.TradeId, amount: resp1.ActualAmount},
|
||||
{token: resp2.Token, tradeID: resp2.TradeId, amount: resp2.ActualAmount},
|
||||
{address: resp1.ReceiveAddress, token: resp1.Token, tradeID: resp1.TradeId, amount: resp1.ActualAmount},
|
||||
{address: resp2.ReceiveAddress, token: resp2.Token, tradeID: resp2.TradeId, amount: resp2.ActualAmount},
|
||||
} {
|
||||
wg.Add(1)
|
||||
go func(token, tradeID string, amount float64) {
|
||||
go func(address, token, tradeID string, amount float64) {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
errs <- OrderProcessing(&request.OrderProcessingRequest{
|
||||
ReceiveAddress: address,
|
||||
Token: token,
|
||||
TradeId: tradeID,
|
||||
Amount: amount,
|
||||
BlockTransactionId: "shared_block",
|
||||
})
|
||||
}(tc.token, tc.tradeID, tc.amount)
|
||||
}(tc.address, tc.token, tc.tradeID, tc.amount)
|
||||
}
|
||||
|
||||
close(start)
|
||||
|
||||
@@ -9,18 +9,20 @@ import (
|
||||
"github.com/assimon/luuu/model/response"
|
||||
)
|
||||
|
||||
// GetCheckoutCounterByTradeId 获取收银台详情,通过订单
|
||||
// GetCheckoutCounterByTradeId returns checkout info for a pending order.
|
||||
func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterResponse, error) {
|
||||
orderInfo, err := data.GetOrderInfoByTradeId(tradeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if orderInfo.ID <= 0 || orderInfo.Status != mdb.StatusWaitPay {
|
||||
return nil, errors.New("不存在待支付订单或已过期!")
|
||||
return nil, errors.New("pending order does not exist or has expired")
|
||||
}
|
||||
|
||||
resp := &response.CheckoutCounterResponse{
|
||||
TradeId: orderInfo.TradeId,
|
||||
ActualAmount: orderInfo.ActualAmount,
|
||||
ReceiveAddress: orderInfo.ReceiveAddress,
|
||||
Token: orderInfo.Token,
|
||||
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||
RedirectUrl: orderInfo.RedirectUrl,
|
||||
|
||||
@@ -1,67 +1,32 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
tron "github.com/assimon/luuu/crypto"
|
||||
"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/telegram"
|
||||
"github.com/assimon/luuu/util/constant"
|
||||
"github.com/assimon/luuu/util/http_client"
|
||||
"github.com/assimon/luuu/util/json"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/assimon/luuu/util/math"
|
||||
"github.com/dromara/carbon/v2"
|
||||
"github.com/gookit/goutil/stdutil"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const UsdtTrc20ApiUri = "https://apilist.tronscanapi.com/api/transfer/trc20"
|
||||
const TRC20_USDT_ID = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
|
||||
|
||||
type UsdtTrc20Resp struct {
|
||||
PageSize int `json:"page_size"`
|
||||
Code int `json:"code"`
|
||||
Data []Data `json:"data"`
|
||||
}
|
||||
|
||||
type TokenInfo struct {
|
||||
TokenID string `json:"tokenId"`
|
||||
TokenAbbr string `json:"tokenAbbr"`
|
||||
TokenName string `json:"tokenName"`
|
||||
TokenDecimal int `json:"tokenDecimal"`
|
||||
TokenCanShow int `json:"tokenCanShow"`
|
||||
TokenType string `json:"tokenType"`
|
||||
TokenLogo string `json:"tokenLogo"`
|
||||
TokenLevel string `json:"tokenLevel"`
|
||||
IssuerAddr string `json:"issuerAddr"`
|
||||
Vip bool `json:"vip"`
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Amount string `json:"amount"`
|
||||
ApprovalAmount string `json:"approval_amount"`
|
||||
BlockTimestamp int64 `json:"block_timestamp"`
|
||||
Block int `json:"block"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Hash string `json:"hash"`
|
||||
Confirmed int `json:"confirmed"`
|
||||
ContractType string `json:"contract_type"`
|
||||
ContracTType int `json:"contractType"`
|
||||
Revert int `json:"revert"`
|
||||
ContractRet string `json:"contract_ret"`
|
||||
EventType string `json:"event_type"`
|
||||
IssueAddress string `json:"issue_address"`
|
||||
Decimals int `json:"decimals"`
|
||||
TokenName string `json:"token_name"`
|
||||
ID string `json:"id"`
|
||||
Direction int `json:"direction"`
|
||||
}
|
||||
|
||||
// Trc20CallBack polls transfers for one wallet and matches them to active orders.
|
||||
func Trc20CallBack(token string, wg *sync.WaitGroup) {
|
||||
func Trc20CallBack(address string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
@@ -69,88 +34,218 @@ func Trc20CallBack(token string, wg *sync.WaitGroup) {
|
||||
}
|
||||
}()
|
||||
|
||||
var innerWg sync.WaitGroup
|
||||
innerWg.Add(2)
|
||||
go checkTrxTransfers(address, &innerWg)
|
||||
go checkTrc20Transfers(address, &innerWg)
|
||||
innerWg.Wait()
|
||||
}
|
||||
|
||||
func checkTrxTransfers(address string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Sugar.Errorf("[TRX][%s] panic recovered: %v", address, err)
|
||||
}
|
||||
}()
|
||||
|
||||
client := http_client.GetHttpClient()
|
||||
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
||||
endTime := carbon.Now().TimestampMilli()
|
||||
url := fmt.Sprintf("https://api.trongrid.io/v1/accounts/%s/transactions", address)
|
||||
|
||||
resp, err := client.R().SetQueryParams(map[string]string{
|
||||
"sort": "-timestamp",
|
||||
"limit": "50",
|
||||
"start": "0",
|
||||
"direction": "2",
|
||||
"db_version": "1",
|
||||
"trc20Id": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
||||
"address": token,
|
||||
"start_timestamp": stdutil.ToString(startTime),
|
||||
"end_timestamp": stdutil.ToString(endTime),
|
||||
}).Get(UsdtTrc20ApiUri)
|
||||
"order_by": "block_timestamp,desc",
|
||||
"limit": "100",
|
||||
"only_to": "true",
|
||||
"min_timestamp": stdutil.ToString(startTime),
|
||||
"max_timestamp": stdutil.ToString(endTime),
|
||||
}).SetHeader("TRON-PRO-API-KEY", config.TRON_GRID_API_KEY).Get(url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
panic(err)
|
||||
panic(fmt.Sprintf("TRX API returned status %d", resp.StatusCode()))
|
||||
}
|
||||
|
||||
var trc20Resp UsdtTrc20Resp
|
||||
if err = json.Cjson.Unmarshal(resp.Body(), &trc20Resp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if trc20Resp.PageSize <= 0 {
|
||||
return
|
||||
success := gjson.GetBytes(resp.Body(), "success").Bool()
|
||||
if !success {
|
||||
panic("TRX API response indicates failure")
|
||||
}
|
||||
|
||||
for _, transfer := range trc20Resp.Data {
|
||||
if transfer.To != token || transfer.ContractRet != "SUCCESS" {
|
||||
for i, transfer := range gjson.GetBytes(resp.Body(), "data").Array() {
|
||||
if transfer.Get("raw_data.contract.0.type").String() != "TransferContract" {
|
||||
continue
|
||||
}
|
||||
if transfer.Get("ret.0.contractRet").String() != "SUCCESS" {
|
||||
continue
|
||||
}
|
||||
|
||||
decimalQuant, err := decimal.NewFromString(transfer.Amount)
|
||||
toAddressHex := transfer.Get("raw_data.contract.0.parameter.value.to_address").String()
|
||||
toBytes, err := hex.DecodeString(toAddressHex)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Sugar.Errorf("[TRX][%s] decode address failed on tx #%d: %v", address, i, err)
|
||||
continue
|
||||
}
|
||||
decimalDivisor := decimal.NewFromFloat(1000000)
|
||||
amount := decimalQuant.Div(decimalDivisor).InexactFloat64()
|
||||
tradeId, err := data.GetTradeIdByWalletAddressAndAmount(token, amount)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if tradeId == "" {
|
||||
if tron.EncodeCheck(toBytes) != address {
|
||||
continue
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(tradeId)
|
||||
rawAmount := transfer.Get("raw_data.contract.0.parameter.value.amount").String()
|
||||
decimalQuant, err := decimal.NewFromString(rawAmount)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("[TRX][%s] parse amount failed on tx #%d: %v", address, i, err)
|
||||
continue
|
||||
}
|
||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1000000)).InexactFloat64(), 2)
|
||||
if amount <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
txID := transfer.Get("txID").String()
|
||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(address, "TRX", amount)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if tradeID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
blockTimestamp := transfer.Get("block_timestamp").Int()
|
||||
createTime := order.CreatedAt.TimestampMilli()
|
||||
if transfer.BlockTimestamp < createTime {
|
||||
panic("orders cannot actually be matched")
|
||||
if blockTimestamp < createTime {
|
||||
log.Sugar.Warnf("[TRX][%s] skip tx %s because block time %d is before order create time %d", address, txID, blockTimestamp, createTime)
|
||||
continue
|
||||
}
|
||||
|
||||
req := &request.OrderProcessingRequest{
|
||||
Token: token,
|
||||
TradeId: tradeId,
|
||||
ReceiveAddress: address,
|
||||
Token: "TRX",
|
||||
TradeId: tradeID,
|
||||
Amount: amount,
|
||||
BlockTransactionId: transfer.Hash,
|
||||
BlockTransactionId: txID,
|
||||
}
|
||||
if err = OrderProcessing(req); err != nil {
|
||||
err = OrderProcessing(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
|
||||
log.Sugar.Infof("[task] skip already resolved transfer, trade_id=%s, block_transaction_id=%s, err=%v", tradeId, transfer.Hash, err)
|
||||
log.Sugar.Infof("[TRX][%s] skip resolved transfer trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
|
||||
continue
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
msgTpl := `
|
||||
<b>馃摙馃摙鏈夋柊鐨勪氦鏄撴敮浠樻垚鍔燂紒</b>
|
||||
<pre>浜ゆ槗鍙凤細%s</pre>
|
||||
<pre>璁㈠崟鍙凤細%s</pre>
|
||||
<pre>璇锋眰鏀粯閲戦锛?f cny</pre>
|
||||
<pre>瀹為檯鏀粯閲戦锛?f usdt</pre>
|
||||
<pre>閽卞寘鍦板潃锛?s</pre>
|
||||
<pre>璁㈠崟鍒涘缓鏃堕棿锛?s</pre>
|
||||
<pre>鏀粯鎴愬姛鏃堕棿锛?s</pre>
|
||||
`
|
||||
msg := fmt.Sprintf(msgTpl, order.TradeId, order.OrderId, order.Amount, order.ActualAmount, order.Token, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString())
|
||||
telegram.SendToBot(msg)
|
||||
sendPaymentNotification(order)
|
||||
}
|
||||
}
|
||||
|
||||
func checkTrc20Transfers(address string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Sugar.Errorf("[TRC20][%s] panic recovered: %v", address, err)
|
||||
}
|
||||
}()
|
||||
|
||||
client := http_client.GetHttpClient()
|
||||
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
||||
endTime := carbon.Now().TimestampMilli()
|
||||
url := fmt.Sprintf("https://api.trongrid.io/v1/accounts/%s/transactions/trc20", address)
|
||||
|
||||
resp, err := client.R().SetQueryParams(map[string]string{
|
||||
"order_by": "block_timestamp,desc",
|
||||
"limit": "100",
|
||||
"only_to": "true",
|
||||
"min_timestamp": stdutil.ToString(startTime),
|
||||
"max_timestamp": stdutil.ToString(endTime),
|
||||
}).SetHeader("TRON-PRO-API-KEY", config.TRON_GRID_API_KEY).Get(url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
panic(fmt.Sprintf("TRC20 API returned status %d", resp.StatusCode()))
|
||||
}
|
||||
|
||||
success := gjson.GetBytes(resp.Body(), "success").Bool()
|
||||
if !success {
|
||||
panic("TRC20 API response indicates failure")
|
||||
}
|
||||
|
||||
for i, transfer := range gjson.GetBytes(resp.Body(), "data").Array() {
|
||||
if transfer.Get("token_info.address").String() != TRC20_USDT_ID {
|
||||
continue
|
||||
}
|
||||
if transfer.Get("to").String() != address {
|
||||
continue
|
||||
}
|
||||
|
||||
valueStr := transfer.Get("value").String()
|
||||
decimalQuant, err := decimal.NewFromString(valueStr)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("[TRC20][%s] parse value failed on tx #%d: %v", address, i, err)
|
||||
continue
|
||||
}
|
||||
tokenDecimals := transfer.Get("token_info.decimals").Int()
|
||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.New(1, int32(tokenDecimals))).InexactFloat64(), 2)
|
||||
if amount <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
txID := transfer.Get("transaction_id").String()
|
||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(address, "USDT", amount)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if tradeID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
blockTimestamp := transfer.Get("block_timestamp").Int()
|
||||
createTime := order.CreatedAt.TimestampMilli()
|
||||
if blockTimestamp < createTime {
|
||||
log.Sugar.Warnf("[TRC20][%s] skip tx %s because block time %d is before order create time %d", address, txID, blockTimestamp, createTime)
|
||||
continue
|
||||
}
|
||||
|
||||
req := &request.OrderProcessingRequest{
|
||||
ReceiveAddress: address,
|
||||
Token: "USDT",
|
||||
TradeId: tradeID,
|
||||
Amount: amount,
|
||||
BlockTransactionId: txID,
|
||||
}
|
||||
err = OrderProcessing(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
|
||||
log.Sugar.Infof("[TRC20][%s] skip resolved transfer trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
|
||||
continue
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sendPaymentNotification(order)
|
||||
}
|
||||
}
|
||||
|
||||
func sendPaymentNotification(order *mdb.Orders) {
|
||||
msg := fmt.Sprintf(
|
||||
"Payment received\nTrade ID: %s\nOrder ID: %s\nOrder Amount: %.2f %s\nReceived Amount: %.2f %s\nAddress: %s\nCreated At: %s\nPaid At: %s",
|
||||
order.TradeId,
|
||||
order.OrderId,
|
||||
order.Amount,
|
||||
strings.ToUpper(order.Currency),
|
||||
order.ActualAmount,
|
||||
strings.ToUpper(order.Token),
|
||||
order.ReceiveAddress,
|
||||
order.CreatedAt.ToDateTimeString(),
|
||||
carbon.Now().ToDateTimeString(),
|
||||
)
|
||||
telegram.SendToBot(msg)
|
||||
}
|
||||
|
||||
+2
-1
@@ -75,7 +75,7 @@ func processExpiredOrders() {
|
||||
if !expired {
|
||||
continue
|
||||
}
|
||||
if err = data.UnLockTransaction(order.Token, order.ActualAmount); err != nil {
|
||||
if err = data.UnLockTransaction(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)
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,7 @@ func sendOrderCallback(order *mdb.Orders) error {
|
||||
OrderId: order.OrderId,
|
||||
Amount: order.Amount,
|
||||
ActualAmount: order.ActualAmount,
|
||||
ReceiveAddress: order.ReceiveAddress,
|
||||
Token: order.Token,
|
||||
BlockTransactionId: order.BlockTransactionId,
|
||||
Status: mdb.StatusPaySuccess,
|
||||
|
||||
+28
-20
@@ -20,13 +20,15 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
||||
defer cleanup()
|
||||
|
||||
order := &mdb.Orders{
|
||||
TradeId: "trade_expired",
|
||||
OrderId: "order_expired",
|
||||
Amount: 1,
|
||||
ActualAmount: 1,
|
||||
Token: "wallet_1",
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
TradeId: "trade_expired",
|
||||
OrderId: "order_expired",
|
||||
Amount: 1,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1,
|
||||
ReceiveAddress: "wallet_1",
|
||||
Token: "USDT",
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
}
|
||||
if err := dao.Mdb.Create(order).Error; err != nil {
|
||||
t.Fatalf("create expired order: %v", err)
|
||||
@@ -34,23 +36,25 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
||||
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)
|
||||
}
|
||||
if err := data.LockTransaction(order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil {
|
||||
if err := data.LockTransaction(order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil {
|
||||
t.Fatalf("lock expired order: %v", err)
|
||||
}
|
||||
|
||||
recentOrder := &mdb.Orders{
|
||||
TradeId: "trade_recent",
|
||||
OrderId: "order_recent",
|
||||
Amount: 1,
|
||||
ActualAmount: 1.01,
|
||||
Token: "wallet_1",
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
TradeId: "trade_recent",
|
||||
OrderId: "order_recent",
|
||||
Amount: 1,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1.01,
|
||||
ReceiveAddress: "wallet_1",
|
||||
Token: "USDT",
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
}
|
||||
if err := dao.Mdb.Create(recentOrder).Error; err != nil {
|
||||
t.Fatalf("create recent order: %v", err)
|
||||
}
|
||||
if err := data.LockTransaction(recentOrder.Token, recentOrder.TradeId, recentOrder.ActualAmount, time.Hour); err != nil {
|
||||
if err := data.LockTransaction(recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.TradeId, recentOrder.ActualAmount, time.Hour); err != nil {
|
||||
t.Fatalf("lock recent order: %v", err)
|
||||
}
|
||||
|
||||
@@ -63,7 +67,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
||||
if expired.Status != mdb.StatusExpired {
|
||||
t.Fatalf("expired order status = %d, want %d", expired.Status, mdb.StatusExpired)
|
||||
}
|
||||
lockTradeID, err := data.GetTradeIdByWalletAddressAndAmount(order.Token, order.ActualAmount)
|
||||
lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(order.ReceiveAddress, order.Token, order.ActualAmount)
|
||||
if err != nil {
|
||||
t.Fatalf("expired order lock lookup: %v", err)
|
||||
}
|
||||
@@ -78,7 +82,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
|
||||
if recent.Status != mdb.StatusWaitPay {
|
||||
t.Fatalf("recent order status = %d, want %d", recent.Status, mdb.StatusWaitPay)
|
||||
}
|
||||
lockTradeID, err = data.GetTradeIdByWalletAddressAndAmount(recentOrder.Token, recentOrder.ActualAmount)
|
||||
lockTradeID, err = data.GetTradeIdByWalletAddressAndAmountAndToken(recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.ActualAmount)
|
||||
if err != nil {
|
||||
t.Fatalf("recent order lock lookup: %v", err)
|
||||
}
|
||||
@@ -95,8 +99,10 @@ func TestProcessExpiredOrdersKeepsPaidOrdersPaid(t *testing.T) {
|
||||
TradeId: "trade_paid",
|
||||
OrderId: "order_paid",
|
||||
Amount: 1,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1,
|
||||
Token: "wallet_1",
|
||||
ReceiveAddress: "wallet_1",
|
||||
Token: "USDT",
|
||||
Status: mdb.StatusPaySuccess,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
BlockTransactionId: "block_paid",
|
||||
@@ -141,8 +147,10 @@ func TestDispatchPendingCallbacksHonorsBackoffAndPersistsSuccess(t *testing.T) {
|
||||
TradeId: "trade_callback",
|
||||
OrderId: "order_callback",
|
||||
Amount: 1,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1,
|
||||
Token: "wallet_1",
|
||||
ReceiveAddress: "wallet_1",
|
||||
Token: "USDT",
|
||||
Status: mdb.StatusPaySuccess,
|
||||
NotifyUrl: server.URL,
|
||||
BlockTransactionId: "block_callback",
|
||||
|
||||
+12
-4
@@ -1,12 +1,20 @@
|
||||
package task
|
||||
|
||||
import "github.com/robfig/cron/v3"
|
||||
import (
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
log.Sugar.Info("[task] Starting task scheduler...")
|
||||
c := cron.New()
|
||||
// 汇率监听
|
||||
c.AddJob("@every 60s", UsdtRateJob{})
|
||||
// trc20钱包监听
|
||||
c.AddJob("@every 5s", ListenTrc20Job{})
|
||||
_, err := c.AddJob("@every 5s", ListenTrc20Job{})
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("[task] Failed to add ListenTrc20Job: %v", err)
|
||||
return
|
||||
}
|
||||
log.Sugar.Info("[task] ListenTrc20Job scheduled successfully (@every 5s)")
|
||||
c.Start()
|
||||
log.Sugar.Info("[task] Task scheduler started")
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/assimon/luuu/model/data"
|
||||
"github.com/assimon/luuu/model/service"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ListenTrc20Job struct {
|
||||
@@ -15,18 +16,24 @@ var gListenTrc20JobLock sync.Mutex
|
||||
func (r ListenTrc20Job) Run() {
|
||||
gListenTrc20JobLock.Lock()
|
||||
defer gListenTrc20JobLock.Unlock()
|
||||
log.Sugar.Debug("[ListenTrc20Job] Job triggered")
|
||||
walletAddress, err := data.GetAvailableWalletAddress()
|
||||
if err != nil {
|
||||
log.Sugar.Error(err)
|
||||
log.Sugar.Errorf("[ListenTrc20Job] Failed to get wallet addresses: %v", err)
|
||||
return
|
||||
}
|
||||
if len(walletAddress) <= 0 {
|
||||
log.Sugar.Debug("[ListenTrc20Job] No available wallet addresses")
|
||||
return
|
||||
}
|
||||
log.Sugar.Infof("[ListenTrc20Job] Found %d wallet addresses to monitor", len(walletAddress))
|
||||
var wg sync.WaitGroup
|
||||
for _, address := range walletAddress {
|
||||
log.Sugar.Infof("[ListenTrc20Job] Listening to address: %s", address.Address)
|
||||
|
||||
wg.Add(1)
|
||||
go service.Trc20CallBack(address.Token, &wg)
|
||||
go service.Trc20CallBack(address.Address, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
log.Sugar.Debug("[ListenTrc20Job] Job completed")
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"github.com/assimon/luuu/config"
|
||||
"github.com/assimon/luuu/util/http_client"
|
||||
"github.com/assimon/luuu/util/json"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/assimon/luuu/util/math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const UsdtRateApiUri = "https://api.coinmarketcap.com/data-api/v3/cryptocurrency/detail/chart"
|
||||
|
||||
type UsdtRateJob struct {
|
||||
}
|
||||
|
||||
type UsdtRateResp struct {
|
||||
Data Data `json:"data"`
|
||||
Status Status `json:"status"`
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
Elapsed string `json:"elapsed"`
|
||||
CreditCount int `json:"credit_count"`
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Points map[string]Points `json:"points"`
|
||||
}
|
||||
|
||||
type Points struct {
|
||||
V []float64 `json:"v"`
|
||||
C []float64 `json:"c"`
|
||||
}
|
||||
|
||||
func (r UsdtRateJob) Run() {
|
||||
client := http_client.GetHttpClient()
|
||||
resp, err := client.R().SetQueryString("id=825&range=1H&convertId=2787").SetHeader("Accept", "application/json").Get(UsdtRateApiUri)
|
||||
if err != nil {
|
||||
log.Sugar.Error("usdt rate get err:", err.Error())
|
||||
return
|
||||
}
|
||||
var usdtResp UsdtRateResp
|
||||
err = json.Cjson.Unmarshal(resp.Body(), &usdtResp)
|
||||
if err != nil {
|
||||
log.Sugar.Error("Unmarshal usdt resp err:", err.Error())
|
||||
return
|
||||
}
|
||||
if usdtResp.Status.ErrorCode != "0" {
|
||||
log.Sugar.Error("usdt resp err:", usdtResp.Status.ErrorMessage)
|
||||
return
|
||||
}
|
||||
for _, points := range usdtResp.Data.Points {
|
||||
if len(points.C) > 0 && points.C[0] > 0 {
|
||||
config.UsdtRate = math.MustParsePrecFloat64(points.C[0], 2)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,8 @@ func WalletList(c tb.Context) error {
|
||||
}
|
||||
var temp []tb.InlineButton
|
||||
btnInfo := tb.InlineButton{
|
||||
Unique: wallet.Token,
|
||||
Text: fmt.Sprintf("%s[%s]", wallet.Token, status),
|
||||
Unique: wallet.Address,
|
||||
Text: fmt.Sprintf("%s[%s]", wallet.Address, status),
|
||||
Data: strutil.MustString(wallet.ID),
|
||||
}
|
||||
bots.Handle(&btnInfo, WalletInfo)
|
||||
@@ -93,7 +93,7 @@ func WalletInfo(c tb.Context) error {
|
||||
bots.Handle(&disableBtn, DisableWallet)
|
||||
bots.Handle(&delBtn, DelWallet)
|
||||
bots.Handle(&backBtn, WalletList)
|
||||
return c.EditOrReply(tokenInfo.Token, &tb.ReplyMarkup{InlineKeyboard: [][]tb.InlineButton{
|
||||
return c.EditOrReply(tokenInfo.Address, &tb.ReplyMarkup{InlineKeyboard: [][]tb.InlineButton{
|
||||
{
|
||||
enableBtn,
|
||||
disableBtn,
|
||||
|
||||
@@ -46,14 +46,21 @@ func RegisterHandle() {
|
||||
// SendToBot 主动发送消息机器人消息
|
||||
func SendToBot(msg string) {
|
||||
go func() {
|
||||
if bots == nil {
|
||||
log.Sugar.Error("[Telegram] bots实例为nil,无法发送消息")
|
||||
return
|
||||
}
|
||||
user := tb.User{
|
||||
ID: config.TgManage,
|
||||
}
|
||||
log.Sugar.Infof("[Telegram] 正在发送消息给用户ID=%d", config.TgManage)
|
||||
_, err := bots.Send(&user, msg, &tb.SendOptions{
|
||||
ParseMode: tb.ModeHTML,
|
||||
})
|
||||
if err != nil {
|
||||
log.Sugar.Error(err)
|
||||
log.Sugar.Errorf("[Telegram] 发送消息失败: userID=%d, err=%v", config.TgManage, err)
|
||||
} else {
|
||||
log.Sugar.Infof("[Telegram] 发送消息成功: userID=%d", config.TgManage)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user