This commit is contained in:
lzx
2025-10-27 16:27:33 +08:00
parent b7c84fd101
commit b1d3e07c36
14 changed files with 2982 additions and 948 deletions

1523
README.html Normal file

File diff suppressed because it is too large Load Diff

1266
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,7 @@
{
"sqlite3": {
"msg_path": "./msg.db"
},
"rmq_config": {
"sub_addr": "amqp://m2pool:m2pool@localhost:5672",
"pay": {
@@ -22,6 +25,13 @@
"pay.withdraw.routing.key"
]
},
"remove": {
"queue": "pay.remove.queue",
"exchange": "pay.exchange",
"routing": [
"pay.remove.routing.key"
]
},
"pay_resp": {
"queue": "pay.auto.return.queue",
"exchange": "pay.exchange",
@@ -42,6 +52,13 @@
"routing": [
"pay.withdraw.return.routing.key"
]
},
"remove_resp": {
"queue": "pay.remove.return.queue",
"exchange": "pay.exchange",
"routing": [
"pay.remove.return.routing.key"
]
}
},
"eth_config": {

12
go.mod
View File

@@ -11,8 +11,17 @@ require (
require (
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
google.golang.org/protobuf v1.36.6 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
require (
@@ -36,6 +45,7 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.36.0 // indirect
modernc.org/sqlite v1.39.1
)

49
go.sum
View File

@@ -44,6 +44,8 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
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/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU=
@@ -75,6 +77,10 @@ github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXi
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.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/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
@@ -115,6 +121,8 @@ github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxd
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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/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/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
@@ -141,6 +149,8 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
@@ -169,12 +179,15 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
@@ -183,6 +196,8 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@@ -191,3 +206,29 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
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/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -6,8 +6,8 @@ import (
)
type IChainServer interface {
AddAddress(address string, msg any)
RemoveAddress(address string)
AddAddress(address string, msg any) error
RemoveAddress(address string) error
Listen(symbol string, ch chan any)
Transfer(symbol string, msg any) error
Stop()
@@ -30,21 +30,23 @@ func (b *BlockChainServer) RegisterChain(name string, chain IChainServer) {
b.chains[name] = chain
}
func (b *BlockChainServer) AddAddress(chain, address string, msg any) {
func (b *BlockChainServer) AddAddress(chain, address string, msg any) error {
if srv, ok := b.chains[chain]; ok {
srv.AddAddress(address, msg)
fmt.Printf("✅ 添加监听地址: chain=%s, address=%s\n", chain, address)
return nil
} else {
fmt.Printf("⚠️ 链未注册: %s\n", chain)
return fmt.Errorf("⚠️ 链未注册: %s\n", chain)
}
}
func (b *BlockChainServer) RemoveAddress(chain, address string) {
func (b *BlockChainServer) RemoveAddress(chain, address string) error {
if srv, ok := b.chains[chain]; ok {
srv.RemoveAddress(address)
fmt.Printf("🗑️ 移除监听地址: chain=%s, address=%s\n", chain, address)
return nil
} else {
fmt.Printf("⚠️ 链未注册: %s\n", chain)
return fmt.Errorf("⚠️ 链未注册: %s\n", chain)
}
}

View File

@@ -0,0 +1,213 @@
package eth
import (
"crypto/ecdsa"
"fmt"
"log"
"m2pool-payment/internal/utils"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)
// BatchTransferItem 批量转账单项
type BatchTransferItem struct {
ToAddress string // 接收地址
Amount float64 // 转账金额
}
// BatchTransferResult 批量转账结果
type BatchTransferResult struct {
TxHash string // 交易哈希
Success bool // 是否成功
TotalAmount float64 // 总转账金额
Count int // 转账笔数
}
// usdt_batch_transfer 批量转账ERC20-USDT
// from: 发送地址
// items: 批量转账列表
// returns: 交易哈希和错误信息
func (e *ETHNode) USDTBatchTransfer(from string, items []BatchTransferItem) (*BatchTransferResult, error) {
if len(items) == 0 {
return nil, fmt.Errorf("批量转账列表不能为空")
}
// 统一转换为小写
from = strings.ToLower(from)
// 计算总金额
var totalAmount float64
for _, item := range items {
if item.Amount <= 0 {
return nil, fmt.Errorf("转账金额必须大于0")
}
totalAmount += item.Amount
}
// 1. 校验钱包USDT余额
balance, err := e.getUSDTBalance(from)
log.Printf("🔄 批量转账 - 检测钱包=%s余额=%.2f USDT", from, balance)
if err != nil {
return nil, fmt.Errorf("获取余额失败: %w", err)
}
if balance < totalAmount {
return nil, fmt.Errorf("余额不足: 余额=%.2f USDT < 需要=%.2f USDT", balance, totalAmount)
}
// 2. 通过from地址前往数据库查找出对应加密后的私钥并解密真实的私钥
originalKey := e.decodePrivatekey(from)
if originalKey == "" {
return nil, fmt.Errorf("无法获取私钥")
}
privateKey, err := crypto.HexToECDSA(originalKey)
if err != nil {
return nil, fmt.Errorf("解析私钥失败: %w", err)
}
// 3. 获得nonce
nonce, err := e.RpcClient.PendingNonceAt(e.Ctx, common.HexToAddress(from))
if err != nil {
return nil, fmt.Errorf("获取nonce失败: %w", err)
}
// 4. 构造批量转账数据
// 使用 transfer(address[], uint256[]) 或多次transfer调用
// 这里使用多次transfer调用的方式因为标准ERC20没有批量转账方法
// 方法1: 构造多次transfer调用适合少量转账
if len(items) <= 3 {
return e.batchTransferMultipleCalls(from, privateKey, nonce, items)
}
// 方法2: 使用合约批量转账(需要部署代理合约,这里简化处理)
// 注意这里实现的是多次transaction方式
return e.batchTransferSeparateTransactions(from, privateKey, nonce, items)
}
// batchTransferMultipleCalls 使用一个交易多次调用transfer需要gas优化
func (e *ETHNode) batchTransferMultipleCalls(from string, privateKey *ecdsa.PrivateKey, nonce uint64, items []BatchTransferItem) (*BatchTransferResult, error) {
// 注意标准ERC20不支持批量transfer这里需要自定义合约
// 或者使用多次独立交易
log.Printf("⚠️ 标准ERC20不支持批量transfer改用多次独立交易")
// 回退到多次独立交易
return e.batchTransferSeparateTransactions(from, privateKey, nonce, items)
}
// batchTransferSeparateTransactions 执行多次独立的transfer交易
func (e *ETHNode) batchTransferSeparateTransactions(from string, privateKey *ecdsa.PrivateKey, nonce uint64, items []BatchTransferItem) (*BatchTransferResult, error) {
var totalAmount float64
var txHashes []string
var allSuccess bool = true
for i, item := range items {
// 构造单个transfer交易
amountBigInt := utils.Float64ToBigIntUSDT(item.Amount)
data, err := e.USDT.ABI.Pack("transfer", common.HexToAddress(strings.ToLower(item.ToAddress)), amountBigInt)
if err != nil {
log.Printf("❌ 批量转账第%d笔打包失败: %v", i+1, err)
allSuccess = false
continue
}
// 获取gas limit
gasLimit, err := e.getGasLimit()
if err != nil {
log.Printf("❌ 批量转账第%d笔获取gasLimit失败: %v", i+1, err)
allSuccess = false
continue
}
// 获取gas费用
maxFeePerGas, maxPriorityFeePerGas, err := e.getEIP1559GasFees()
var txHash string
if err != nil {
// 回退到传统gas price
gasPrice, err := e.getSuggestGasPrice()
if err != nil {
log.Printf("❌ 批量转账第%d笔获取gasPrice失败: %v", i+1, err)
allSuccess = false
continue
}
tx := types.NewTransaction(nonce+uint64(i), e.USDT.Address, big.NewInt(0), gasLimit, gasPrice, data)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
if err != nil {
log.Printf("❌ 批量转账第%d笔签名失败: %v", i+1, err)
allSuccess = false
continue
}
txHash = signedTx.Hash().Hex()
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
log.Printf("❌ 批量转账第%d笔发送失败: %v", i+1, err)
allSuccess = false
continue
}
} else {
// 使用EIP-1559交易
ethBalance, err := e.getETHBlance(from)
if err != nil {
log.Printf("❌ 批量转账第%d笔获取ETH余额失败: %v", i+1, err)
allSuccess = false
continue
}
maxGasCost := new(big.Int).Mul(new(big.Int).SetUint64(gasLimit), maxFeePerGas)
if ethBalance.Cmp(maxGasCost) == -1 {
log.Printf("❌ 批量转账第%d笔ETH余额不足", i+1)
allSuccess = false
continue
}
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: e.NetId,
Nonce: nonce + uint64(i),
GasTipCap: maxPriorityFeePerGas,
GasFeeCap: maxFeePerGas,
Gas: gasLimit,
To: &e.USDT.Address,
Value: big.NewInt(0),
Data: data,
})
signedTx, err := types.SignTx(tx, types.NewLondonSigner(e.NetId), privateKey)
if err != nil {
log.Printf("❌ 批量转账第%d笔签名失败: %v", i+1, err)
allSuccess = false
continue
}
txHash = signedTx.Hash().Hex()
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
log.Printf("❌ 批量转账第%d笔发送失败: %v", i+1, err)
allSuccess = false
continue
}
}
txHashes = append(txHashes, txHash)
totalAmount += item.Amount
log.Printf("✅ 批量转账第%d笔已提交: %s, 金额=%.2f USDT, 收款地址=%s",
i+1, txHash, item.Amount, strings.ToLower(item.ToAddress))
}
log.Printf("📊 批量转账完成: 总计%d笔, 成功%d笔, 总金额=%.2f USDT",
len(items), len(txHashes), totalAmount)
return &BatchTransferResult{
TxHash: strings.Join(txHashes, ","),
Success: allSuccess && len(txHashes) == len(items),
TotalAmount: totalAmount,
Count: len(txHashes),
}, nil
}

View File

@@ -0,0 +1,101 @@
# ERC20-USDT 批量转账功能
## 功能说明
该文件 `batch_transfer.go` 提供了 ERC20-USDT 的批量转账功能,支持从同一个发送地址向多个不同的接收地址转账。
## 主要功能
### 1. 批量转账类型
```go
type BatchTransferItem struct {
ToAddress string // 接收地址
Amount float64 // 转账金额
}
type BatchTransferResult struct {
TxHash string // 交易哈希(多个用逗号分隔)
Success bool // 是否成功
TotalAmount float64 // 总转账金额
Count int // 转账笔数
}
```
### 2. 使用方法
```go
// 1. 准备批量转账列表
items := []eth.BatchTransferItem{
{ToAddress: "0xRecipient1", Amount: 100.0},
{ToAddress: "0xRecipient2", Amount: 200.0},
{ToAddress: "0xRecipient3", Amount: 50.0},
}
// 2. 调用批量转账
fromAddress := "0xYourAddress"
result, err := ethNode.USDTBatchTransfer(fromAddress, items)
if err != nil {
log.Fatalf("批量转账失败: %v", err)
}
// 3. 处理结果
fmt.Printf("批量转账完成: %d笔, 总金额: %.2f USDT", result.Count, result.TotalAmount)
fmt.Printf("交易哈希: %s", result.TxHash)
```
## 工作原理
由于标准 ERC20 合约不支持批量转账,本实现采用以下策略:
1. **多次独立交易**:对每笔转账创建一个独立的 ERC20 `transfer` 交易
2. **Nonce 管理**:自动管理 nonce确保交易按顺序广播
3. **Gas 费用**:支持 EIP-1559 动态费用和传统 gas price
4. **错误处理**:单笔失败不影响其他交易,返回成功和失败的详细统计
## 注意事项
### 1. Gas 费用
- 每笔转账需要独立的 gas 费用(约 65,000 gas
- 批量转账 10 笔需要约 650,000 gas
- 确保发送地址有足够的 ETH 作为 gas 费用
### 2. 余额检查
- 函数会自动检查 USDT 余额是否足够
- 如果余额不足,会返回错误并终止转账
### 3. 部分成功
- 如果某些转账失败,函数会继续执行其他转账
- 返回结果中包含成功笔数和详细交易哈希
### 4. 网络拥堵
- 在高网络拥堵时,某些交易可能被推迟
- 建议监控所有交易状态
## 性能优化建议
如果需要更高效的批量转账,考虑:
1. **部署批量转账代理合约**:实现一个合约方法 `batchTransfer(address[] to, uint256[] amounts)`
2. **使用多签钱包**:减少私钥管理风险
3. **Gas 优化**:使用更低的 gas price 分批发送
## 示例输出
```
🔄 批量转账 - 检测钱包=0x...,余额=1000.00 USDT
✅ 批量转账第1笔已提交: 0xabc123..., 金额=100.00 USDT, 收款地址=0x...
✅ 批量转账第2笔已提交: 0xdef456..., 金额=200.00 USDT, 收款地址=0x...
✅ 批量转账第3笔已提交: 0x789ghi..., 金额=50.00 USDT, 收款地址=0x...
📊 批量转账完成: 总计3笔, 成功3笔, 总金额=350.00 USDT
```
## 限制
- 标准 ERC20 不支持真正的批量转账(单笔交易)
- 需要确保发送地址有足够的 ETH 作为 gas 费用
- 交易按顺序发送,可能在高负载时较慢

View File

@@ -125,27 +125,25 @@ func NewETHNode(cfg message.ETHConfig, decodeKey string) (*ETHNode, error) {
}
// ============================ 抽象接口 ============================
func (e *ETHNode) AddAddress(address string, rmq_msg any) {
func (e *ETHNode) AddAddress(address string, rmq_msg any) error {
// 统一转换为小写
address = strings.ToLower(address)
log.Printf("新增钱包监听消息:%v", rmq_msg)
e.ListenAddresses.Store(address, true)
e.mu.Lock()
if len(e.RmqMsgs[address]) == 0 {
e.RmqMsgs[address] = []any{rmq_msg}
} else {
e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg)
}
e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg)
e.mu.Unlock()
return nil
}
func (e *ETHNode) RemoveAddress(address string) {
func (e *ETHNode) RemoveAddress(address string) error {
// 统一转换为小写
address = strings.ToLower(address)
e.ListenAddresses.Delete(address)
e.mu.Lock()
delete(e.RmqMsgs, address)
e.mu.Unlock()
return nil
}
func (e *ETHNode) Listen(symbol string, ch chan any) {
@@ -174,6 +172,10 @@ func (e *ETHNode) Transfer(symbol string, msg any) error {
return nil
}
func (e *ETHNode) Stop() {
e.Cancel()
}
// ============================ rpc节点方法 ============================
func (e *ETHNode) getETHBlance(address string) (*big.Int, error) {
@@ -221,12 +223,11 @@ func (e *ETHNode) getUSDTBalance(address string) (float64, error) {
return bal, nil
}
func (e *ETHNode) getBlockHeight() (uint64, error) {
header, err := e.RpcClient.HeaderByNumber(e.Ctx, nil)
if err != nil {
return 0, fmt.Errorf("failed to get latest block header: %w", err)
}
return header.Number.Uint64(), nil
func (e *ETHNode) getGasLimit() (uint64, error) {
// 对于ERC20转账使用固定的gas limit
// 通常ERC20 transfer需要约65,000-100,000 gas
// 这里设置为80,000足够覆盖大部分情况
return 80000, nil
}
func (e *ETHNode) getSuggestGasPrice() (*big.Int, error) {
@@ -235,9 +236,56 @@ func (e *ETHNode) getSuggestGasPrice() (*big.Int, error) {
if err != nil {
return nil, fmt.Errorf("get suggest-gasprice error:%v", err)
}
// 设置gas price上限避免在网络拥堵时费用过高
// 这里设置为20 Gwei (20 * 10^9 wei)
maxGasPrice := new(big.Int).SetUint64(20000000000) // 20 Gwei
if gasPrice.Cmp(maxGasPrice) > 0 {
log.Printf("⚠️ 建议gas price过高 (%v Gwei),使用上限 20 Gwei", new(big.Int).Div(gasPrice, big.NewInt(1000000000)))
return maxGasPrice, nil
}
log.Printf("✅ 使用建议gas price: %v Gwei", new(big.Int).Div(gasPrice, big.NewInt(1000000000)))
return gasPrice, nil
}
// getEIP1559GasFees 获取EIP-1559的gas费用参数
func (e *ETHNode) getEIP1559GasFees() (*big.Int, *big.Int, error) {
ctx := context.Background()
// 获取基础费用
latestBlock, err := e.RpcClient.BlockByNumber(ctx, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to get latest block: %w", err)
}
baseFee := latestBlock.BaseFee()
if baseFee == nil {
return nil, nil, fmt.Errorf("base fee not available")
}
// 设置优先级费用tip这里设置为2 Gwei
maxPriorityFeePerGas := new(big.Int).SetUint64(2000000000) // 2 Gwei
// 计算最大费用 = 基础费用 + 优先级费用
maxFeePerGas := new(big.Int).Add(baseFee, maxPriorityFeePerGas)
// 设置最大费用上限为30 Gwei
maxFeeLimit := new(big.Int).SetUint64(30000000000) // 30 Gwei
if maxFeePerGas.Cmp(maxFeeLimit) > 0 {
log.Printf("⚠️ 计算的最大费用过高 (%v Gwei),使用上限 30 Gwei", new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)))
maxFeePerGas = maxFeeLimit
}
log.Printf("✅ EIP-1559 Gas费用: BaseFee=%v Gwei, MaxPriorityFee=%v Gwei, MaxFee=%v Gwei",
new(big.Int).Div(baseFee, big.NewInt(1000000000)),
new(big.Int).Div(maxPriorityFeePerGas, big.NewInt(1000000000)),
new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)))
return maxFeePerGas, maxPriorityFeePerGas, nil
}
// ============================ 业务方法 ============================
func (e *ETHNode) listen_usdt(ch chan any) error {
fmt.Println("🔍 ETH 开始监听 USDT Transfer 事件...")
@@ -257,6 +305,7 @@ func (e *ETHNode) listen_usdt(ch chan any) error {
fmt.Println("✅ 订阅成功")
// 处理事件
for {
select {
case err := <-sub.Err():
fmt.Println("⚠️ 订阅异常,准备重连:", err)
@@ -578,7 +627,6 @@ func (e *ETHNode) decodePrivatekey(address string) string {
}
// 使用key解密
privateKey := encryptedKey // 实际使用时替换成具体的解密代码
// fmt.Println(privateKey)
return privateKey
}
@@ -624,7 +672,6 @@ func (e *ETHNode) usdt_transfer(msg any) error {
if originalKey == "" {
return fmt.Errorf("failed to query privatekey")
}
fmt.Println(originalKey)
privateKey, err := crypto.HexToECDSA(originalKey)
if err != nil {
return fmt.Errorf("failed to parse private key: %w", err)
@@ -640,41 +687,125 @@ func (e *ETHNode) usdt_transfer(msg any) error {
if err != nil {
return fmt.Errorf("failed to pack transfer data: %w", err)
}
gasPrice, err := e.getSuggestGasPrice() // 获得当前建议gasPrice
gasLimit, err := e.getGasLimit() // 获得gasLimit
if err != nil {
return fmt.Errorf("get suggest-gasprice error:%v", err)
return fmt.Errorf("get gas limit error:%v", err)
}
eth_balance, err := e.getETHBlance(final_from) // 获得钱包eth余额
// 获取EIP-1559 gas费用参数
maxFeePerGas, maxPriorityFeePerGas, err := e.getEIP1559GasFees()
if err != nil {
log.Printf("⚠️ 获取EIP-1559费用失败回退到传统gas price: %v", err)
// 回退到传统gas price
gasPrice, err := e.getSuggestGasPrice()
if err != nil {
return fmt.Errorf("get suggest-gasprice error:%v", err)
}
eth_balance, err := e.getETHBlance(final_from)
if err != nil {
return fmt.Errorf("%w", err)
}
gasLimit_b := new(big.Int).SetUint64(gasLimit)
gas := new(big.Int).Mul(gasLimit_b, gasPrice)
// 计算gas费用以ETH为单位
gasInETH := new(big.Float).SetInt(gas)
gasInETH.Quo(gasInETH, new(big.Float).SetInt64(1000000000000000000))
log.Printf("💰 传统Gas费用预估: Limit=%d, Price=%v Gwei, 总费用=%.6f ETH",
gasLimit,
new(big.Int).Div(gasPrice, big.NewInt(1000000000)),
gasInETH)
// 判断钱包eth是否支持本次交易gas费用
if eth_balance.Cmp(gas) == -1 {
ethBalanceInETH := new(big.Float).SetInt(eth_balance)
ethBalanceInETH.Quo(ethBalanceInETH, new(big.Float).SetInt64(1000000000000000000))
return fmt.Errorf("❌ 地址 %s ETH余额不足: %.6f ETH < %.6f ETH (gas费用)",
final_from, ethBalanceInETH, gasInETH)
}
// 构造传统交易
tx := types.NewTransaction(
nonce,
e.USDT.Address,
big.NewInt(0),
gasLimit,
gasPrice,
data,
)
// 签名并发送传统交易
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
if err != nil {
return fmt.Errorf("failed to sign transaction: %w", err)
}
txHash := signedTx.Hash().Hex()
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
return fmt.Errorf("failed to send transaction: %w", err)
}
log.Printf("✅ 传统交易已提交至mempool%s金额:%.2f USDT, 手续费:%.6f ETH", txHash, amount, gasInETH)
return nil
}
// 使用EIP-1559交易
eth_balance, err := e.getETHBlance(final_from)
if err != nil {
return fmt.Errorf("%w", err)
}
var gasLimit uint64 = 100000
gasLimit_b := new(big.Int).SetUint64(gasLimit)
gas := new(big.Int).Mul(gasLimit_b, gasPrice)
// 计算最大可能的gas费用
maxGasCost := new(big.Int).Mul(new(big.Int).SetUint64(gasLimit), maxFeePerGas)
// 计算gas费用以ETH为单位
maxGasCostInETH := new(big.Float).SetInt(maxGasCost)
maxGasCostInETH.Quo(maxGasCostInETH, new(big.Float).SetInt64(1000000000000000000))
log.Printf("💰 EIP-1559 Gas费用预估: Limit=%d, MaxFee=%v Gwei, MaxPriorityFee=%v Gwei, 最大费用=%.6f ETH",
gasLimit,
new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)),
new(big.Int).Div(maxPriorityFeePerGas, big.NewInt(1000000000)),
maxGasCostInETH)
// 判断钱包eth是否支持本次交易gas费用
if eth_balance.Cmp(gas) == -1 {
return fmt.Errorf("address=%s balance less than gas=%v(wei)", final_from, eth_balance)
if eth_balance.Cmp(maxGasCost) == -1 {
ethBalanceInETH := new(big.Float).SetInt(eth_balance)
ethBalanceInETH.Quo(ethBalanceInETH, new(big.Float).SetInt64(1000000000000000000))
return fmt.Errorf("❌ 地址 %s ETH余额不足: %.6f ETH < %.6f ETH (最大gas费用)",
final_from, ethBalanceInETH, maxGasCostInETH)
}
// 构造发送到 USDT 合约地址的交易
tx := types.NewTransaction(
nonce,
e.USDT.Address, // 发送到USDT合约地址
big.NewInt(0), // value为0ERC20转账不需要ETH
gasLimit, // GasLimit设置为100000ERC20转账需要更多gas
gasPrice, // GasPrice: 20 Gwei
data, // 附加数据transfer方法调用
)
// 6, 签名交易并获得txHash
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
// txHash := signedTx.Hash().Hex() // 通过签名信息解析出交易hash
// 构造EIP-1559交易
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: e.NetId,
Nonce: nonce,
GasTipCap: maxPriorityFeePerGas,
GasFeeCap: maxFeePerGas,
Gas: gasLimit,
To: &e.USDT.Address,
Value: big.NewInt(0),
Data: data,
})
// 6, 签名EIP-1559交易并获得txHash
signedTx, err := types.SignTx(tx, types.NewLondonSigner(e.NetId), privateKey)
if err != nil {
return fmt.Errorf("failed to sign transaction: %w", err)
return fmt.Errorf("failed to sign EIP-1559 transaction: %w", err)
}
// 7, 发送交易
txHash := signedTx.Hash().Hex()
// 7, 发送EIP-1559交易
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
return fmt.Errorf("failed to send transaction: %w", err)
return fmt.Errorf("failed to send EIP-1559 transaction: %w", err)
}
log.Printf("✅ EIP-1559交易已提交至mempool%s金额:%.2f USDT, 最大手续费:%.6f ETH", txHash, amount, maxGasCostInETH)
// // 8, 构造交易消息
// tx_msg := message.Tx_msg{
// TxType: tx_type,
@@ -692,7 +823,3 @@ func (e *ETHNode) usdt_transfer(msg any) error {
// e.UnConfirmTxs[txHash] = tx_msg
return nil
}
func (e *ETHNode) Stop() {
e.Cancel()
}

120
internal/db/sqlite.go Normal file
View File

@@ -0,0 +1,120 @@
package db
import (
"database/sql"
"fmt"
_ "modernc.org/sqlite" // 导入驱动
)
type SQLite struct {
DB *sql.DB
}
// 初始化连接
func NewSQLite(path string) (*SQLite, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("open sqlite failed: %v", err)
}
return &SQLite{DB: db}, nil
}
// 关闭数据库
func (s *SQLite) Close() {
if s.DB != nil {
s.DB.Close()
}
}
// 通用新建表
func (s *SQLite) Exec_(sql string) error {
_, err := s.DB.Exec(sql)
if err != nil {
return fmt.Errorf("Exec DB error: %w", err)
}
return nil
}
// 通用查询方法(返回[]map[string]any
func (s *SQLite) Query_(query string, args ...any) ([]map[string]any, error) {
rows, err := s.DB.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("query error: %v", err)
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return nil, err
}
var results []map[string]any
for rows.Next() {
// 创建一个与列数相同的 slice 用于 Scan
values := make([]any, len(columns))
valuePtrs := make([]any, len(columns))
for i := range values {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
return nil, err
}
// 将行转换为 map
rowMap := make(map[string]any)
for i, col := range columns {
val := values[i]
if b, ok := val.([]byte); ok {
rowMap[col] = string(b)
} else {
rowMap[col] = val
}
}
results = append(results, rowMap)
}
return results, nil
}
// 插入数据(支持事务和批量)
func (s *SQLite) Insert(sqlStr string, args ...[]any) error {
tx, err := s.DB.Begin()
if err != nil {
return err
}
stmt, err := tx.Prepare(sqlStr)
if err != nil {
return err
}
defer stmt.Close()
for _, a := range args {
_, err = stmt.Exec(a...)
if err != nil {
tx.Rollback()
return fmt.Errorf("exec insert error: %v", err)
}
}
return tx.Commit()
}
// 更新数据
func (s *SQLite) Update(sqlStr string, args ...any) (int64, error) {
res, err := s.DB.Exec(sqlStr, args...)
if err != nil {
return 0, fmt.Errorf("update error: %v", err)
}
rows, _ := res.RowsAffected()
return rows, nil
}
// 删除数据
func (s *SQLite) Delete(sqlStr string, args ...any) (int64, error) {
res, err := s.DB.Exec(sqlStr, args...)
if err != nil {
return 0, fmt.Errorf("delete error: %v", err)
}
rows, _ := res.RowsAffected()
return rows, nil
}

View File

@@ -188,20 +188,20 @@ func compressFile(filePath string) error {
}
// LogTopup 记录充值消息
func LogTopup(address string, status string, amount float64, txHash string, blockHeight uint64) {
func LogTopup(toAddress string, status string, amount float64, txHash string, blockHeight uint64) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(address)
lf, err := txLogger.getOrCreateLogFile(toAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
content := fmt.Sprintf("%s [topup]-[%s] | 金额: %.6f | 交易哈希: %s | 区块高度: %d | 地址: %s",
timestamp, status, amount, txHash, blockHeight, address)
content := fmt.Sprintf("%s [topup]-[%s] | 金额: %.6f | 交易哈希: %s | 区块高度: %d | ToAddress: %s",
timestamp, status, amount, txHash, blockHeight, toAddress)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
@@ -209,21 +209,21 @@ func LogTopup(address string, status string, amount float64, txHash string, bloc
}
// LogWithdraw 记录提现消息
func LogWithdraw(queueId string, status string, amount float64, from string, to string, txHash string, blockHeight uint64) {
func LogWithdraw(toAddress string, status string, amount float64, fromAddress string, txHash string, blockHeight uint64) {
if txLogger == nil {
return
}
// 使用 queueId 作为文件名
lf, err := txLogger.getOrCreateLogFile(queueId)
// 使用 toAddress 作为文件名
lf, err := txLogger.getOrCreateLogFile(toAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
content := fmt.Sprintf("%s [withdraw]-[%s] | 金额: %.6f | From: %s | To: %s | 交易哈希: %s | 区块高度: %d",
timestamp, status, amount, from, to, txHash, blockHeight)
content := fmt.Sprintf("%s [withdraw]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d",
timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
@@ -231,21 +231,21 @@ func LogWithdraw(queueId string, status string, amount float64, from string, to
}
// LogPay 记录支付消息
func LogPay(orderId string, queueId string, status string, amount float64, from string, to string, txHash string, blockHeight uint64) {
func LogPay(toAddress string, status string, amount float64, fromAddress string, txHash string, blockHeight uint64, orderId string, queueId string) {
if txLogger == nil {
return
}
// 使用 orderId 作为文件名
lf, err := txLogger.getOrCreateLogFile(orderId)
// 使用 toAddress 作为文件名
lf, err := txLogger.getOrCreateLogFile(toAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
content := fmt.Sprintf("%s [pay]-[%s] | 订单ID: %s | 队列ID: %s | 金额: %.6f | From: %s | To: %s | 交易哈希: %s | 区块高度: %d",
timestamp, status, orderId, queueId, amount, from, to, txHash, blockHeight)
content := fmt.Sprintf("%s [pay]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d | OrderId: %s | QueueId: %s",
timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight, orderId, queueId)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)

View File

@@ -4,19 +4,26 @@ import "time"
// 配置文件结构
type Config struct {
SQLite3 SQLite3 `json:"sqlite3"`
RMQConfig RMQConfig `json:"rmq_config"`
ETHConfig ETHConfig `json:"eth_config"`
TRONConfig TRONConfig `json:"tron_config"`
}
type SQLite3 struct {
MsgPath string `json:"msg_path"`
}
type RMQConfig struct {
SubAddr string `json:"sub_addr"` // 监听地址
PayConfig QueueConfig `json:"pay"` // 支付
TopUpConfig QueueConfig `json:"topup"` // 充值
WithdrawConfig QueueConfig `json:"withdraw"` // 提现
RemoveConfig QueueConfig `json:"remove"` // 移除监听
PayRespConfig QueueConfig `json:"pay_resp"` // 支付回复
TopUpRespConfig QueueConfig `json:"topup_resp"` // 充值回复
WithdrawRespConfig QueueConfig `json:"withdraw_resp"` // 提现回复
RemoveRespConfig QueueConfig `json:"remove_resp"` // 移除监听回复
}
type QueueConfig struct {
@@ -50,7 +57,7 @@ type DbConfig struct {
ConnMaxLife time.Duration `json:"connMaxLife"` // 连接最大存活时间
}
// =======================================================================
// =============================== type0 ===============================
// 接收的充值消息
type TopupMsg_req struct {
Chain string `json:"chain"` // 链名称
@@ -71,6 +78,7 @@ type TopupMsg_resp struct {
BlockHeight uint64 `json:"block_height"` // 区块高度
}
// =============================== type1 ===============================
// 接收的提现消息
type WithdrawMsg_req struct {
QueueId string `json:"queue_id"`
@@ -86,16 +94,17 @@ type WithdrawMsg_req struct {
// 返回提现结果消息
type WithdrawMsg_resp struct {
QueueId string `json:"queue_id"`
Status int `json:"status"`
Amount float64 `json:"amount"`
Chain string `json:"chain"` // 链名称
Symbol string `json:"symbol"` // 币种
Status int `json:"status"`
Amount float64 `json:"amount"`
TxHash string `json:"tx_hash"`
FromAddress string `json:"from_address"` // 来源地址
ToAddress string `json:"to_address"` // 目标地址
BlockHeight uint64 `json:"block_height"` // 区块高度
}
// =============================== type2 ===============================
// 接收到的支付消息
type PayMsg_req struct {
QueueId string `json:"queue_id"`
@@ -123,6 +132,27 @@ type PayMsg_resp struct {
BlockHeight uint64 `json:"block_height"` // 区块高度
}
// =============================== type3 ===============================
// 接收到的删除监听地址消息
type RemoveListenMsg_req struct {
MsgType int `json:"msg_type"`
Chain string `json:"chain"`
Symbol string `json:"symbol"`
Address string `json:"address"`
Timestamp uint64 `json:"timestamp"`
Sign string `json:"sign"`
}
// 返回收到的删除监听地址消息
type RemoveListenMsg_resp struct {
MsgType int `json:"msg_type"`
Chain string `json:"chain"`
Symbol string `json:"symbol"`
Address string `json:"address"`
Status int `json:"status"` // 0失败 1成功
}
// =====================================================================
// 节点通用消息结构
type Tx_msg struct {
TxType int `json:"tx_type"` // 转账类型0充值1提现2支付

View File

@@ -8,6 +8,7 @@ import (
"m2pool-payment/internal/blockchain"
"m2pool-payment/internal/blockchain/eth"
"m2pool-payment/internal/crypto"
"m2pool-payment/internal/db"
"m2pool-payment/internal/logger"
message "m2pool-payment/internal/msg"
rmq "m2pool-payment/internal/queue"
@@ -15,6 +16,7 @@ import (
"os/signal"
"strings"
"syscall"
"time"
)
const MSG_KEY string = "9f3c7a12"
@@ -32,6 +34,7 @@ type ServerCtx struct {
Config message.Config
blockChainServer *blockchain.BlockChainServer
rmqServer *rmq.RabbitMQServer
sqlitedb db.SQLite
}
var s_ctx ServerCtx
@@ -76,6 +79,113 @@ func initBlockChainServer() {
log.Println("✅ 区块链服务初始化完成")
}
func loadSQLiteData() {
err1 := loadTopupReqMsg()
if err1 != nil {
log.Fatalf("load topup msg err:%v", err1)
}
err2 := loadWithdrawReqMsg()
if err2 != nil {
log.Fatalf("load withdraw msg err:%v", err2)
}
err3 := loadPayReqMsg()
if err3 != nil {
log.Fatalf("load pay msg err:%v", err3)
}
}
func loadTopupReqMsg() error {
sql := `SELECT chain, symbol, timestamp, to_addr FROM msg_topup_req;`
rows, err := s_ctx.sqlitedb.DB.Query(sql)
if err != nil {
return fmt.Errorf("query history topup-msg error: %w", err)
}
defer rows.Close()
var topupReq_msg message.TopupMsg_req
hasData := false
for rows.Next() {
hasData = true
if err := rows.Scan(&topupReq_msg.Chain, &topupReq_msg.Symbol, &topupReq_msg.Timestamp, &topupReq_msg.Address); err != nil {
return err
}
s_ctx.blockChainServer.AddAddress(topupReq_msg.Chain, topupReq_msg.Address, topupReq_msg)
}
if !hasData {
log.Println("Msg_topup_req`s msg has not data, doesn`t need to load.")
return nil
}
// 在遍历完所有数据后检查是否发生了错误
if err := rows.Err(); err != nil {
log.Printf("Error encountered while iterating over rows: %v", err)
}
return nil
}
func loadWithdrawReqMsg() error {
sql := `SELECT queueId, chain, symbol, timestamp, from_addr, to_addr, amount FROM msg_withdraw_req;`
rows, err := s_ctx.sqlitedb.DB.Query(sql)
if err != nil {
return fmt.Errorf("query history withdraw-msg error: %w", err)
}
defer rows.Close()
var withdrawReq_msg message.WithdrawMsg_req
hasData := false
for rows.Next() {
hasData = true
// var chain, symbol, to_addr string
// var timestamp uint64
if err := rows.Scan(&withdrawReq_msg.QueueId, &withdrawReq_msg.Chain, &withdrawReq_msg.Symbol, &withdrawReq_msg.Timestamp, &withdrawReq_msg.FromAddress, &withdrawReq_msg.ToAddress, &withdrawReq_msg.Amount); err != nil {
return err
}
s_ctx.blockChainServer.AddAddress(withdrawReq_msg.Chain, withdrawReq_msg.ToAddress, withdrawReq_msg)
}
if !hasData {
log.Println("Msg_withdraw_req`s msg has not data, doesn`t need to load.")
return nil
}
// 在遍历完所有数据后检查是否发生了错误
if err := rows.Err(); err != nil {
log.Printf("Error encountered while iterating over rows: %v", err)
}
return nil
}
func loadPayReqMsg() error {
sql := `SELECT queueId, chain, symbol, timestamp, from_addr, to_addr, amount, orderId FROM msg_pay_req;`
rows, err := s_ctx.sqlitedb.DB.Query(sql)
if err != nil {
return fmt.Errorf("query history pay-msg error: %w", err)
}
defer rows.Close()
var payReq_msg message.PayMsg_req
hasData := false
for rows.Next() {
hasData = true
if err := rows.Scan(&payReq_msg.QueueId, &payReq_msg.Chain, &payReq_msg.Symbol, &payReq_msg.Timestamp, &payReq_msg.FromAddress, &payReq_msg.ToAddress, &payReq_msg.Amount, &payReq_msg.OrderId); err != nil {
return err
}
s_ctx.blockChainServer.AddAddress(payReq_msg.Chain, payReq_msg.ToAddress, payReq_msg)
}
if !hasData {
log.Println("Msg_pay_req`s msg has not data, doesn`t need to load.")
return nil
}
// 在遍历完所有数据后检查是否发生了错误
if err := rows.Err(); err != nil {
log.Printf("Error encountered while iterating over rows: %v", err)
}
return nil
}
func initRmqServer() {
// 初始化rmq服务
rmq_server, err := rmq.NewRabbitMQServer(s_ctx.Config.RMQConfig)
@@ -88,6 +198,21 @@ func initRmqServer() {
log.Printf("✅ RabbitMQ服务初始化完成: %s", s_ctx.Config.RMQConfig.SubAddr)
}
func initSQLite(sqlite3_file string) {
// 初始化sqlite3数据库
sqlite3, err := db.NewSQLite(sqlite3_file)
if err != nil {
log.Fatalf("connect sqlite3 error:%v", err)
return
}
sqlByte, err := os.ReadFile("../public/SQLite3.sql")
if err != nil {
log.Fatalf("open sql file error: %s", "../public/SQLite3.sql")
}
sqlite3.Exec_(string(sqlByte))
s_ctx.sqlitedb = *sqlite3
}
func handleTopupMsg() {
s_ctx.rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
msg.Address = strings.ToLower(msg.Address)
@@ -109,7 +234,32 @@ func handleTopupMsg() {
}
// 添加监听地址
s_ctx.blockChainServer.AddAddress(msg.Chain, msg.Address, msg)
// go func() {
err := s_ctx.blockChainServer.AddAddress(msg.Chain, msg.Address, msg)
if err != nil {
log.Printf("❌ 添加监听地址失败: %v", err)
// 发送失败响应
err = s_ctx.rmqServer.PublishTopupResp(message.TopupMsg_resp{
Address: msg.Address,
Status: STATUS_FAILED,
Chain: msg.Chain,
Symbol: msg.Symbol,
Amount: 0,
TxHash: "",
})
if err != nil {
log.Printf("❌ 发布充值失败响应失败: %v", err)
}
return
}
// }()
// 将新增数据写入sqlite
insert_sql := `INSERT OR REPLACE INTO msg_topup_req (chain, symbol, timestamp, to_addr) VALUES (?, ?, ?, ?)`
data := []any{msg.Chain, msg.Symbol, msg.Timestamp, msg.Address}
err = s_ctx.sqlitedb.Insert(insert_sql, data)
if err != nil {
log.Printf("❌ 插入 msg_req 失败: %v, data: %+v", err, data)
}
}
}
@@ -147,6 +297,30 @@ func handleWithdrawMsg() {
Symbol: msg.Symbol,
TxHash: "",
})
return // 转账失败时直接返回,不进入链上确认流程
}
// go func() {
err = s_ctx.blockChainServer.AddAddress(msg.Chain, msg.ToAddress, msg)
if err != nil {
log.Printf("❌ 添加监听地址失败: %v", err)
// 发送失败响应
s_ctx.rmqServer.PublishWithdrawResp(message.WithdrawMsg_resp{
QueueId: msg.QueueId,
Status: STATUS_FAILED,
Amount: msg.Amount,
Chain: msg.Chain,
Symbol: msg.Symbol,
TxHash: "",
})
return
}
// }()
// 将新增数据写入sqlite
insert_sql := `INSERT OR REPLACE INTO msg_withdraw_req (queueId, chain, symbol, timestamp, from_addr, to_addr, amount) VALUES (?, ?, ?, ?, ?, ?, ?)`
data := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Timestamp, msg.FromAddress, msg.ToAddress, msg.Amount}
err = s_ctx.sqlitedb.Insert(insert_sql, data)
if err != nil {
log.Printf("❌ 插入 withdraw_req 失败: %v, data: %+v", err, data)
}
}
}
@@ -187,6 +361,31 @@ func handlePayMsg() {
OrderId: msg.OrderId,
TxHash: "",
})
return // 转账失败时直接返回,不进入链上确认流程
}
// go func() {
err = s_ctx.blockChainServer.AddAddress(msg.Chain, msg.ToAddress, msg)
if err != nil {
log.Printf("❌ 添加监听地址失败: %v", err)
// 发送失败响应
s_ctx.rmqServer.PublishPayResp(message.PayMsg_resp{
QueueId: msg.QueueId,
Status: STATUS_FAILED,
Amount: msg.Amount,
Chain: msg.Chain,
Symbol: msg.Symbol,
OrderId: msg.OrderId,
TxHash: "",
})
return
}
// }()
// 将新增数据写入sqlite
insert_sql := `INSERT OR REPLACE INTO msg_pay_req (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, orderId) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
data := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Timestamp, msg.FromAddress, msg.ToAddress, msg.Amount, msg.OrderId}
err = s_ctx.sqlitedb.Insert(insert_sql, data)
if err != nil {
log.Printf("❌ 插入 pay_req 失败: %v, data: %+v", err, data)
}
}
}
@@ -233,32 +432,87 @@ func handleChainEvent(chainEventCh chan any) {
err := s_ctx.rmqServer.PublishTopupResp(msg)
if err != nil {
log.Printf("❌ 发送充值响应失败: %v", err)
return
}
go func() {
// 插入响应数据
sql := `INSERT INTO msg_topup_resp (chain, symbol, timestamp, to_addr, amount, height, txHash, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
data := []any{msg.Chain, msg.Symbol, time.Now().Unix(), msg.Address, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status}
err := s_ctx.sqlitedb.Insert(sql, data)
if err != nil {
log.Printf("❌ 插入 topup_resp 失败: %v", err)
return
}
}()
case message.WithdrawMsg_resp:
// 提现确认
log.Printf("✅ [链上] 提现确认: QueueId=%s, Amount=%.2f, TxHash=%s, Status=%d",
msg.QueueId, msg.Amount, msg.TxHash, msg.Status)
// 记录交易日志
logger.LogWithdraw(msg.QueueId, "确认", msg.Amount, msg.FromAddress,
msg.ToAddress, msg.TxHash, msg.BlockHeight)
logger.LogWithdraw(msg.ToAddress, "确认", msg.Amount, msg.FromAddress, msg.TxHash, msg.BlockHeight)
err := s_ctx.rmqServer.PublishWithdrawResp(msg)
if err != nil {
log.Printf("❌ 发送提现响应失败: %v", err)
return
}
go func() {
// 插入响应数据
sql := `INSERT INTO msg_withdraw_resp (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, height, txHash, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
data := []any{msg.QueueId, msg.Chain, msg.Symbol, time.Now().Unix(), msg.FromAddress, msg.ToAddress, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status}
err := s_ctx.sqlitedb.Insert(sql, data)
if err != nil {
log.Printf("❌ 插入 withdraw_resp 失败: %v", err)
return
}
// 删除对应数据
del_sql := `DELETE FROM msg_withdraw_req WHERE queueId = ?;`
count, err := s_ctx.sqlitedb.Delete(del_sql, msg.QueueId)
if err != nil {
log.Printf("❌ 清理 withdraw_req 失败: %v, queueId=%s", err, msg.QueueId)
} else if count == 0 {
log.Printf("⚠️ 未找到要删除的 withdraw_req 记录: queueId=%s", msg.QueueId)
} else {
log.Printf("✅ 清理 withdraw_req 成功: 删除了 %d 条记录, queueId=%s", count, msg.QueueId)
}
}()
case message.PayMsg_resp:
// 支付确认
log.Printf("✅ [链上] 支付确认: QueueId=%s, OrderId=%s, Amount=%.2f, TxHash=%s, Status=%d",
msg.QueueId, msg.OrderId, msg.Amount, msg.TxHash, msg.Status)
// 记录交易日志
logger.LogPay(msg.OrderId, msg.QueueId, "确认", msg.Amount, msg.FromAddress,
msg.ToAddress, msg.TxHash, msg.BlockHeight)
logger.LogPay(msg.ToAddress, "确认", msg.Amount, msg.FromAddress, msg.TxHash, msg.BlockHeight, msg.OrderId, msg.QueueId)
err := s_ctx.rmqServer.PublishPayResp(msg)
if err != nil {
log.Printf("❌ 发送支付响应失败: %v", err)
return
}
go func() {
// 插入响应数据
sql := `INSERT INTO msg_pay_resp (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, height, txHash, status, orderId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
data := []any{msg.QueueId, msg.Chain, msg.Symbol, time.Now().Unix(), msg.FromAddress, msg.ToAddress, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status, msg.OrderId}
err := s_ctx.sqlitedb.Insert(sql, data)
if err != nil {
log.Printf("❌ 插入 pay_resp 失败: %v", err)
return
}
// 删除对应数据
del_sql := `DELETE FROM msg_pay_req WHERE queueId = ?;`
count, err := s_ctx.sqlitedb.Delete(del_sql, msg.QueueId)
if err != nil {
log.Printf("❌ 清理 pay_req 失败: %v, queueId=%s", err, msg.QueueId)
} else if count == 0 {
log.Printf("⚠️ 未找到要删除的 pay_req 记录: queueId=%s", msg.QueueId)
} else {
log.Printf("✅ 清理 pay_req 成功: 删除了 %d 条记录, queueId=%s", count, msg.QueueId)
}
}()
default:
log.Printf("⚠️ 未知消息类型: %T", event)
}
@@ -283,6 +537,11 @@ func Start(msgKey string) {
// ================== 初始化区块链节点 ==================
initBlockChainServer()
// ================== 初始化SQLite3 ==================
initSQLite(s_ctx.Config.SQLite3.MsgPath)
// 读取历史信息
loadSQLiteData()
// ================== 初始化 RabbitMQ 服务 ==================
initRmqServer()

71
public/SQLite3.sql Normal file
View File

@@ -0,0 +1,71 @@
CREATE TABLE IF NOT EXISTS msg_topup_req (
chain TEXT,
symbol TEXT,
timestamp INTEGER,
to_addr TEXT,
PRIMARY KEY(to_addr)
);
CREATE TABLE IF NOT EXISTS msg_withdraw_req (
queueId TEXT,
chain TEXT,
symbol TEXT,
timestamp INTEGER,
from_addr TEXT,
to_addr TEXT,
amount NUMERIC,
PRIMARY KEY(queueId)
);
CREATE TABLE IF NOT EXISTS msg_pay_req (
queueId TEXT,
chain TEXT,
symbol TEXT,
timestamp INTEGER,
from_addr TEXT,
to_addr TEXT,
amount NUMERIC,
orderId TEXT,
PRIMARY KEY(queueId)
);
CREATE TABLE IF NOT EXISTS msg_topup_resp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain TEXT,
symbol TEXT,
timestamp INTEGER,
to_addr TEXT,
amount NUMERIC,
height INTEGER,
TxHash TEXT,
status INTEGER
);
CREATE TABLE IF NOT EXISTS msg_withdraw_resp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
queueId TEXT,
chain TEXT,
symbol TEXT,
timestamp INTEGER,
from_addr TEXT,
to_addr TEXT,
amount NUMERIC,
height INTEGER,
txHash TEXT,
status INTEGER
);
CREATE TABLE IF NOT EXISTS msg_pay_resp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
queueId TEXT,
chain TEXT,
symbol TEXT,
timestamp INTEGER,
from_addr TEXT,
to_addr TEXT,
amount NUMERIC,
height INTEGER,
txHash TEXT,
orderId TEXT,
status INTEGER
);