update
This commit is contained in:
1523
README.html
Normal file
1523
README.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
12
go.mod
@@ -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
49
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
213
internal/blockchain/eth/batch_transfer.go
Normal file
213
internal/blockchain/eth/batch_transfer.go
Normal 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
|
||||
}
|
||||
101
internal/blockchain/eth/batch_transfer_example.md
Normal file
101
internal/blockchain/eth/batch_transfer_example.md
Normal 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 费用
|
||||
- 交易按顺序发送,可能在高负载时较慢
|
||||
@@ -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为0(ERC20转账不需要ETH)
|
||||
gasLimit, // GasLimit设置为100000(ERC20转账需要更多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
120
internal/db/sqlite.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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支付
|
||||
|
||||
@@ -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
71
public/SQLite3.sql
Normal 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
|
||||
);
|
||||
Reference in New Issue
Block a user