parent
392943654b
commit
541471f401
@ -1,71 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Port int `yaml:"port"`
|
||||
} `yaml:"server"`
|
||||
|
||||
DB struct {
|
||||
Driver string `yaml:"driver"`
|
||||
DSN string `yaml:"dsn"`
|
||||
} `yaml:"db"`
|
||||
|
||||
Chain struct {
|
||||
RPCURL string `yaml:"rpc_url"`
|
||||
ContractAddress string `yaml:"contract_address"`
|
||||
ChainID int64 `yaml:"chain_id"`
|
||||
SignerPrivateKey string `yaml:"signer_private_key"`
|
||||
ConfigAdminPrivateKey string `yaml:"config_admin_private_key"`
|
||||
} `yaml:"chain"`
|
||||
|
||||
Tron struct {
|
||||
FullNodeURL string `yaml:"full_node_url"`
|
||||
ContractBase58 string `yaml:"contract_base58"`
|
||||
OwnerBase58 string `yaml:"owner_base58"`
|
||||
PrivateKeyHex string `yaml:"private_key_hex"`
|
||||
FeeLimit int64 `yaml:"fee_limit"`
|
||||
} `yaml:"tron"`
|
||||
|
||||
Indexer struct {
|
||||
PollInterval int `yaml:"poll_interval"`
|
||||
} `yaml:"indexer"`
|
||||
}
|
||||
|
||||
var Cfg Config
|
||||
|
||||
// Load loads configuration from YAML file
|
||||
func Load(configPath string) {
|
||||
if configPath == "" {
|
||||
configPath = "config/config.yaml"
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: could not read config file %s: %v, using defaults\n", configPath, err)
|
||||
setDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &Cfg); err != nil {
|
||||
fmt.Printf("Warning: could not parse config: %v, using defaults\n", err)
|
||||
setDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Loaded config from %s\n", configPath)
|
||||
}
|
||||
|
||||
func setDefaults() {
|
||||
Cfg.Server.Port = 8080
|
||||
Cfg.DB.Driver = "sqlite"
|
||||
Cfg.DB.DSN = "redpacket.db"
|
||||
Cfg.Chain.ChainID = 1
|
||||
Cfg.Indexer.PollInterval = 5
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
db:
|
||||
driver: sqlite
|
||||
dsn: redpacket.db
|
||||
|
||||
chain:
|
||||
rpc_url: "https://eth.llamarpc.com"
|
||||
contract_address: "0xYourRedPacketContractAddress"
|
||||
chain_id: 1
|
||||
signer_private_key: "your-signer-private-key-here"
|
||||
config_admin_private_key: "your-config-admin-private-key-here"
|
||||
|
||||
tron:
|
||||
full_node_url: ""
|
||||
contract_base58: ""
|
||||
owner_base58: ""
|
||||
private_key_hex: ""
|
||||
fee_limit: 10000000000
|
||||
|
||||
indexer:
|
||||
poll_interval: 5
|
||||
@ -1,68 +0,0 @@
|
||||
module redpacket
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/ethereum/go-ethereum v1.14.12
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/google/uuid v1.6.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/sqlite v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/consensys/bavard v0.1.13 // indirect
|
||||
github.com/consensys/gnark-crypto v0.12.1 // indirect
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
|
||||
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/holiman/uint256 v1.3.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mmcloughlin/addchain v0.4.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/supranational/blst v0.3.13 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
rsc.io/tmplfunc v0.0.3 // indirect
|
||||
)
|
||||
@ -1,260 +0,0 @@
|
||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
|
||||
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||
github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
|
||||
github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
|
||||
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
||||
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
|
||||
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
|
||||
github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M=
|
||||
github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I=
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs=
|
||||
github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI=
|
||||
github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
|
||||
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
|
||||
github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4=
|
||||
github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY=
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A=
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
|
||||
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/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=
|
||||
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
|
||||
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
|
||||
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
|
||||
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
|
||||
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
|
||||
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
|
||||
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
|
||||
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
|
||||
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
|
||||
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
|
||||
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
|
||||
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk=
|
||||
github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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.5.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
|
||||
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
|
||||
@ -1,49 +0,0 @@
|
||||
package authctx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const opUserIDKey = "opUserID"
|
||||
|
||||
type userIDContextKey struct{}
|
||||
|
||||
func WithCurrentUserID(ctx context.Context, userID string) context.Context {
|
||||
return context.WithValue(ctx, userIDContextKey{}, strings.TrimSpace(userID))
|
||||
}
|
||||
|
||||
func CurrentUserID(ctx context.Context) (string, error) {
|
||||
if ctx == nil {
|
||||
return "", fmt.Errorf("request context is nil")
|
||||
}
|
||||
if userID, ok := ctx.Value(userIDContextKey{}).(string); ok && strings.TrimSpace(userID) != "" {
|
||||
return strings.TrimSpace(userID), nil
|
||||
}
|
||||
if userID, ok := ctx.Value(opUserIDKey).(string); ok && strings.TrimSpace(userID) != "" {
|
||||
return strings.TrimSpace(userID), nil
|
||||
}
|
||||
return "", fmt.Errorf("op user id missing in context")
|
||||
}
|
||||
|
||||
func BindCurrentUserID(c *gin.Context) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("gin context is nil")
|
||||
}
|
||||
userID := strings.TrimSpace(c.GetString(opUserIDKey))
|
||||
if userID == "" {
|
||||
if value := c.Request.Context().Value(opUserIDKey); value != nil {
|
||||
if fromCtx, ok := value.(string); ok {
|
||||
userID = strings.TrimSpace(fromCtx)
|
||||
}
|
||||
}
|
||||
}
|
||||
if userID == "" {
|
||||
return fmt.Errorf("op user id missing in context")
|
||||
}
|
||||
c.Request = c.Request.WithContext(WithCurrentUserID(c.Request.Context(), userID))
|
||||
return nil
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"redpacket/internal/service"
|
||||
"redpacket/pkg/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
adminSvc *service.AdminService
|
||||
}
|
||||
|
||||
func NewAdminHandler(adminSvc *service.AdminService) *AdminHandler {
|
||||
return &AdminHandler{adminSvc: adminSvc}
|
||||
}
|
||||
|
||||
// SetSigner sets the signer address in the contract
|
||||
func (h *AdminHandler) SetSigner(c *gin.Context) {
|
||||
var req struct {
|
||||
SignerAddress string `json:"signer_address" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetSigner(c.Request.Context(), req.SignerAddress); err != nil {
|
||||
resp.InternalError(c, "failed to set signer: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "signer address updated successfully"})
|
||||
}
|
||||
|
||||
// SetToken configures allowed token
|
||||
func (h *AdminHandler) SetToken(c *gin.Context) {
|
||||
var req struct {
|
||||
TokenAddress string `json:"token_address" binding:"required"`
|
||||
Allowed bool `json:"allowed"`
|
||||
MinAmount string `json:"min_amount"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetToken(c.Request.Context(), req.TokenAddress, req.Allowed, req.MinAmount); err != nil {
|
||||
resp.InternalError(c, "failed to set token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "token configuration updated"})
|
||||
}
|
||||
|
||||
// SetExpiry sets default expiry duration
|
||||
func (h *AdminHandler) SetExpiry(c *gin.Context) {
|
||||
var req struct {
|
||||
ExpirySeconds int64 `json:"expiry_seconds" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetExpiry(c.Request.Context(), req.ExpirySeconds); err != nil {
|
||||
resp.InternalError(c, "failed to set expiry: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "expiry duration updated"})
|
||||
}
|
||||
|
||||
// SetAllowAllTokens sets whether all tokens are allowed
|
||||
func (h *AdminHandler) SetAllowAllTokens(c *gin.Context) {
|
||||
var req struct {
|
||||
AllowAll bool `json:"allow_all"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetAllowAllTokens(c.Request.Context(), req.AllowAll); err != nil {
|
||||
resp.InternalError(c, "failed to update allow all tokens: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "allow all tokens setting updated"})
|
||||
}
|
||||
|
||||
// SetNativeTokenEnabled enables/disables native token (ETH/TRX)
|
||||
func (h *AdminHandler) SetNativeTokenEnabled(c *gin.Context) {
|
||||
var req struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetNativeTokenEnabled(c.Request.Context(), req.Enabled); err != nil {
|
||||
resp.InternalError(c, "failed to update native token setting: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "native token setting updated"})
|
||||
}
|
||||
|
||||
// ParseTxEvents manually parses events from a transaction hash (for debugging)
|
||||
func (h *AdminHandler) ParseTxEvents(c *gin.Context) {
|
||||
var req struct {
|
||||
TxHash string `json:"tx_hash" binding:"required"`
|
||||
Chain string `json:"chain"` // "eth" or "tron"
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.adminSvc.ParseTxEvents(c.Request.Context(), req.TxHash, req.Chain)
|
||||
if err != nil {
|
||||
resp.InternalError(c, "failed to parse tx events: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"redpacket/internal/authctx"
|
||||
"redpacket/internal/service"
|
||||
"redpacket/pkg/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RedPacketHandler struct {
|
||||
rpSvc *service.RedPacketService
|
||||
}
|
||||
|
||||
func NewRedPacketHandler(rpSvc *service.RedPacketService) *RedPacketHandler {
|
||||
return &RedPacketHandler{rpSvc: rpSvc}
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) CreateOrder(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req service.CreateOrderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.CreateOrder(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) CreatedCallback(c *gin.Context) {
|
||||
var req service.CreatedCallbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.rpSvc.CreatedCallback(c.Request.Context(), &req); err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) Detail(c *gin.Context) {
|
||||
packetID := c.Query("packet_id")
|
||||
if packetID == "" {
|
||||
resp.BadRequest(c, "packet_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
detail, err := h.rpSvc.GetDetail(c.Request.Context(), packetID)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusNotFound, 404, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, detail)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) ClaimSign(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PacketID string `json:"packet_id" binding:"required"`
|
||||
Claimer string `json:"claimer" binding:"required"`
|
||||
RandomSeed string `json:"random_seed"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.IssueClaimSign(c.Request.Context(), req.PacketID, req.Claimer, req.RandomSeed)
|
||||
if err != nil {
|
||||
resp.InternalError(c, "failed to issue claim signature: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) ClaimResult(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req service.ClaimResultRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.rpSvc.ClaimResult(c.Request.Context(), &req); err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) WalletBindChallenge(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req service.WalletBindChallengeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.IssueWalletBindChallenge(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) WalletBindConfirm(c *gin.Context) {
|
||||
var req service.WalletBindConfirmRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.ConfirmWalletBind(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) WalletBindDetail(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
chainType := c.Query("chain_type")
|
||||
walletAddress := c.Query("wallet_address")
|
||||
if chainType == "" || walletAddress == "" {
|
||||
resp.BadRequest(c, "chain_type and wallet_address are required")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.GetWalletBinding(c.Request.Context(), "", chainType, walletAddress)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusNotFound, 404, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RedPacket struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
BizID string `gorm:"uniqueIndex;size:64" json:"biz_id"`
|
||||
ChainType string `gorm:"index;size:16" json:"chain_type"` // EVM, TRON
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
ContractAddress string `json:"contract_address"`
|
||||
CreatorUserID string `gorm:"size:64" json:"creator_user_id"`
|
||||
CreatorWallet string `gorm:"size:66" json:"creator_wallet"`
|
||||
GroupID string `gorm:"index;size:64" json:"group_id"`
|
||||
ScopeType string `gorm:"size:20" json:"scope_type"` // GROUP, DIRECT, PUBLIC
|
||||
ReceiverUserID string `gorm:"size:64" json:"receiver_user_id"`
|
||||
ReceiverUserIDs string `gorm:"type:text" json:"receiver_user_ids"`
|
||||
PacketType int32 `json:"packet_type"` // 0=fixed, 1=random, 2=transfer
|
||||
Token string `gorm:"size:66" json:"token"`
|
||||
TotalAmount string `gorm:"size:50" json:"total_amount"`
|
||||
TotalShares int32 `json:"total_shares"`
|
||||
ClaimedAmount string `gorm:"size:50" json:"claimed_amount"`
|
||||
ClaimedShares int32 `json:"claimed_shares"`
|
||||
ExpiryAt int64 `json:"expiry_at"`
|
||||
TxHash string `gorm:"size:66" json:"tx_hash"`
|
||||
Status string `gorm:"size:20" json:"status"` // PENDING, ACTIVE, EXPIRED, COMPLETED, REFUNDED
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RedPacketClaim struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;index:idx_packet_user;size:32" json:"packet_id"`
|
||||
UserID string `gorm:"index;index:idx_packet_user;size:64" json:"user_id"`
|
||||
ClaimerWallet string `gorm:"size:66" json:"claimer_wallet"`
|
||||
AuthNonce string `gorm:"size:32" json:"auth_nonce"`
|
||||
ClaimTxHash string `gorm:"uniqueIndex;size:66" json:"claim_tx_hash"`
|
||||
ClaimedAmount string `gorm:"size:50" json:"claimed_amount"`
|
||||
BlockNumber uint64 `json:"block_number"`
|
||||
Status string `gorm:"size:20" json:"status"` // PENDING, CONFIRMED, FAILED
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RedPacketClaimAuth struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
Claimer string `gorm:"size:66" json:"claimer"`
|
||||
AuthNonce string `gorm:"uniqueIndex;size:32" json:"auth_nonce"`
|
||||
RandomSeed string `gorm:"size:32" json:"random_seed"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
Signature string `gorm:"size:132" json:"signature"`
|
||||
Used bool `json:"used"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type RedPacketRefund struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
RefundTo string `gorm:"size:66" json:"refund_to"`
|
||||
TxHash string `gorm:"uniqueIndex;size:66" json:"tx_hash"`
|
||||
Amount string `gorm:"size:50" json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type WalletBindingChallenge struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
ChallengeID string `gorm:"uniqueIndex;size:64" json:"challenge_id"`
|
||||
UserID string `gorm:"index;size:64" json:"user_id"`
|
||||
ChainType string `gorm:"index;size:16" json:"chain_type"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
WalletAddress string `gorm:"index;size:128" json:"wallet_address"`
|
||||
Nonce string `gorm:"size:64" json:"nonce"`
|
||||
Message string `gorm:"type:text" json:"message"`
|
||||
Protocol string `gorm:"size:32" json:"protocol"`
|
||||
SignMethod string `gorm:"size:32" json:"sign_method"`
|
||||
Status string `gorm:"size:20" json:"status"` // PENDING, VERIFIED, EXPIRED, FAILED
|
||||
Signature string `gorm:"type:text" json:"signature"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type WalletBinding struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID string `gorm:"index:idx_user_chain_wallet,unique;size:64" json:"user_id"`
|
||||
ChainType string `gorm:"index:idx_user_chain_wallet,unique;size:16" json:"chain_type"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
WalletAddress string `gorm:"index:idx_user_chain_wallet,unique;size:128" json:"wallet_address"`
|
||||
Status string `gorm:"size:20" json:"status"` // ACTIVE, REVOKED
|
||||
ChallengeID string `gorm:"size:64" json:"challenge_id"`
|
||||
VerifiedAt time.Time `json:"verified_at"`
|
||||
RevokedAt *time.Time `json:"revoked_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@ -1,251 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"redpacket/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
CreateRedPacket(ctx context.Context, rp *model.RedPacket) error
|
||||
GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error)
|
||||
GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error)
|
||||
UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error
|
||||
UpdateRedPacketStatus(ctx context.Context, packetID, status string) error
|
||||
UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error
|
||||
CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error
|
||||
GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error)
|
||||
MarkClaimAuthUsed(ctx context.Context, authNonce string) error
|
||||
GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error)
|
||||
GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error)
|
||||
SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error
|
||||
GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, error)
|
||||
SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error
|
||||
CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
|
||||
GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error)
|
||||
UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
|
||||
UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error
|
||||
GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func New(db *gorm.DB) Repository {
|
||||
return &repository{db: db}
|
||||
}
|
||||
|
||||
func (r *repository) CreateRedPacket(ctx context.Context, rp *model.RedPacket) error {
|
||||
return r.db.WithContext(ctx).Create(rp).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error) {
|
||||
var rp model.RedPacket
|
||||
err := r.db.WithContext(ctx).Where("biz_id = ?", bizID).First(&rp).Error
|
||||
return &rp, err
|
||||
}
|
||||
|
||||
func (r *repository) GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error) {
|
||||
var rp model.RedPacket
|
||||
err := r.db.WithContext(ctx).Where("packet_id = ?", packetID).First(&rp).Error
|
||||
return &rp, err
|
||||
}
|
||||
|
||||
func (r *repository) UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacket{}).
|
||||
Where("biz_id = ?", rp.BizID).
|
||||
Updates(map[string]interface{}{
|
||||
"chain_type": rp.ChainType,
|
||||
"packet_id": rp.PacketID,
|
||||
"tx_hash": rp.TxHash,
|
||||
"chain_id": rp.ChainID,
|
||||
"contract_address": rp.ContractAddress,
|
||||
"group_id": rp.GroupID,
|
||||
"scope_type": rp.ScopeType,
|
||||
"receiver_user_id": rp.ReceiverUserID,
|
||||
"receiver_user_ids": rp.ReceiverUserIDs,
|
||||
"status": rp.Status,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateRedPacketStatus(ctx context.Context, packetID, status string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacket{}).
|
||||
Where("packet_id = ?", packetID).
|
||||
Update("status", status).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var rp model.RedPacket
|
||||
if err := tx.Where("packet_id = ?", packetID).First(&rp).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalClaimed := addNumericStrings(rp.ClaimedAmount, claimedAmount)
|
||||
nextShares := rp.ClaimedShares + 1
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"claimed_amount": totalClaimed,
|
||||
"claimed_shares": nextShares,
|
||||
"updated_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
}
|
||||
if status != "" {
|
||||
updates["status"] = status
|
||||
}
|
||||
|
||||
return tx.Model(&model.RedPacket{}).
|
||||
Where("id = ?", rp.ID).
|
||||
Updates(updates).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *repository) CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error {
|
||||
return r.db.WithContext(ctx).Create(auth).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error) {
|
||||
var auth model.RedPacketClaimAuth
|
||||
err := r.db.WithContext(ctx).Where("packet_id = ? AND claimer = ? AND used = false", packetID, claimer).First(&auth).Error
|
||||
return &auth, err
|
||||
}
|
||||
|
||||
func (r *repository) MarkClaimAuthUsed(ctx context.Context, authNonce string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacketClaimAuth{}).
|
||||
Where("auth_nonce = ?", authNonce).
|
||||
Update("used", true).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error) {
|
||||
var claim model.RedPacketClaim
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("packet_id = ? AND claimer_wallet = ?", packetID, claimer).
|
||||
Order("created_at desc").
|
||||
First(&claim).Error
|
||||
return &claim, err
|
||||
}
|
||||
|
||||
func (r *repository) GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error) {
|
||||
var claim model.RedPacketClaim
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("packet_id = ? AND user_id = ?", packetID, userID).
|
||||
Order("created_at desc").
|
||||
First(&claim).Error
|
||||
return &claim, err
|
||||
}
|
||||
|
||||
func (r *repository) SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error {
|
||||
if claim.UserID != "" {
|
||||
var existing model.RedPacketClaim
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("packet_id = ? AND user_id = ?", claim.PacketID, claim.UserID).
|
||||
First(&existing).Error
|
||||
if err == nil {
|
||||
claim.ID = existing.ID
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacketClaim{}).
|
||||
Where("id = ?", existing.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"claimer_wallet": existing.ClaimerWallet,
|
||||
"auth_nonce": claim.AuthNonce,
|
||||
"claim_tx_hash": claim.ClaimTxHash,
|
||||
"claimed_amount": claim.ClaimedAmount,
|
||||
"block_number": claim.BlockNumber,
|
||||
"status": claim.Status,
|
||||
"updated_at": claim.UpdatedAt,
|
||||
}).Error
|
||||
}
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "claim_tx_hash"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"user_id",
|
||||
"packet_id",
|
||||
"claimer_wallet",
|
||||
"auth_nonce",
|
||||
"claimed_amount",
|
||||
"block_number",
|
||||
"status",
|
||||
"updated_at",
|
||||
}),
|
||||
}).Create(claim).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, error) {
|
||||
var claims []model.RedPacketClaim
|
||||
err := r.db.WithContext(ctx).Where("packet_id = ?", packetID).Order("created_at desc").Find(&claims).Error
|
||||
return claims, err
|
||||
}
|
||||
|
||||
func (r *repository) SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error {
|
||||
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "tx_hash"}},
|
||||
DoNothing: true,
|
||||
}).Create(refund).Error
|
||||
}
|
||||
|
||||
func (r *repository) CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
|
||||
return r.db.WithContext(ctx).Create(challenge).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error) {
|
||||
var challenge model.WalletBindingChallenge
|
||||
err := r.db.WithContext(ctx).Where("challenge_id = ?", challengeID).First(&challenge).Error
|
||||
return &challenge, err
|
||||
}
|
||||
|
||||
func (r *repository) UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
|
||||
return r.db.WithContext(ctx).Model(&model.WalletBindingChallenge{}).
|
||||
Where("challenge_id = ?", challenge.ChallengeID).
|
||||
Updates(map[string]interface{}{
|
||||
"status": challenge.Status,
|
||||
"signature": challenge.Signature,
|
||||
"verified_at": challenge.VerifiedAt,
|
||||
"updated_at": challenge.UpdatedAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error {
|
||||
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "user_id"},
|
||||
{Name: "chain_type"},
|
||||
{Name: "wallet_address"},
|
||||
},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"chain_id",
|
||||
"status",
|
||||
"challenge_id",
|
||||
"verified_at",
|
||||
"revoked_at",
|
||||
"updated_at",
|
||||
}),
|
||||
}).Create(binding).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error) {
|
||||
var binding model.WalletBinding
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("user_id = ? AND chain_type = ? AND wallet_address = ? AND status = ?", userID, chainType, walletAddress, "ACTIVE").
|
||||
First(&binding).Error
|
||||
return &binding, err
|
||||
}
|
||||
|
||||
func addNumericStrings(current, delta string) string {
|
||||
left := new(big.Int)
|
||||
if current != "" {
|
||||
left.SetString(current, 10)
|
||||
}
|
||||
right := new(big.Int)
|
||||
if delta != "" {
|
||||
right.SetString(delta, 10)
|
||||
}
|
||||
return new(big.Int).Add(left, right).String()
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"redpacket/internal/chain"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// AdminService handles administrative operations on the RedPacket contract
|
||||
type AdminService struct {
|
||||
ethClient *chain.ChainClient
|
||||
tronClient *chain.TronClient
|
||||
}
|
||||
|
||||
func NewAdminService(ethClient *chain.ChainClient, tronClient *chain.TronClient) *AdminService {
|
||||
return &AdminService{
|
||||
ethClient: ethClient,
|
||||
tronClient: tronClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AdminService) SetSigner(ctx context.Context, signerAddress string) error {
|
||||
if s.ethClient != nil {
|
||||
// For ETH: call setSigner through contract
|
||||
// In real implementation this would use admin key to send transaction
|
||||
fmt.Printf("ETH: Setting signer to %s (mock)\n", signerAddress)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setSigner", signerAddress)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) SetToken(ctx context.Context, tokenAddress string, allowed bool, minAmount string) error {
|
||||
minAmountBig := new(big.Int)
|
||||
if minAmount != "" {
|
||||
minAmountBig.SetString(minAmount, 10)
|
||||
} else {
|
||||
minAmountBig.SetInt64(0)
|
||||
}
|
||||
|
||||
if s.ethClient != nil {
|
||||
fmt.Printf("ETH: Setting token %s allowed=%v minAmount=%s (mock)\n", tokenAddress, allowed, minAmount)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setAllowedToken", tokenAddress, allowed, minAmountBig)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) SetExpiry(ctx context.Context, expirySeconds int64) error {
|
||||
if s.ethClient != nil {
|
||||
fmt.Printf("ETH: Setting default expiry to %d seconds (mock)\n", expirySeconds)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setDefaultExpiryDuration", expirySeconds)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) SetAllowAllTokens(ctx context.Context, allowAll bool) error {
|
||||
if s.ethClient != nil {
|
||||
fmt.Printf("ETH: Setting allowAllTokens=%v (mock)\n", allowAll)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setAllowAllTokens", allowAll)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) SetNativeTokenEnabled(ctx context.Context, enabled bool) error {
|
||||
if s.ethClient != nil {
|
||||
fmt.Printf("ETH: Setting native token enabled=%v (mock)\n", enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setNativeTokenEnabled", enabled)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) ParseTxEvents(ctx context.Context, txHash, chain string) (map[string]interface{}, error) {
|
||||
if chain == "tron" && s.tronClient != nil {
|
||||
return map[string]interface{}{
|
||||
"chain": "tron",
|
||||
"tx_hash": txHash,
|
||||
"status": "parsed",
|
||||
"note": "TRON event parsing not fully implemented in this version",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if s.ethClient != nil {
|
||||
txHashBytes := common.HexToHash(txHash)
|
||||
events, err := s.ethClient.ParseTransactionReceipt(ctx, txHashBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eventList := make([]map[string]interface{}, len(events))
|
||||
for i, e := range events {
|
||||
eventList[i] = map[string]interface{}{
|
||||
"name": e.Name,
|
||||
"data": e.Data,
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"chain": "eth",
|
||||
"tx_hash": txHash,
|
||||
"events": eventList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no client available for chain: %s", chain)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,386 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"redpacket/internal/authctx"
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newTestService(t *testing.T) (*RedPacketService, repository.Repository) {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("gorm.Open() error = %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&model.RedPacket{},
|
||||
&model.RedPacketClaim{},
|
||||
&model.RedPacketClaimAuth{},
|
||||
&model.RedPacketRefund{},
|
||||
&model.WalletBindingChallenge{},
|
||||
&model.WalletBinding{},
|
||||
); err != nil {
|
||||
t.Fatalf("AutoMigrate() error = %v", err)
|
||||
}
|
||||
|
||||
repo := repository.New(db)
|
||||
svc := NewRedPacketService(repo, nil, nil, "")
|
||||
return svc, repo
|
||||
}
|
||||
|
||||
func seedWalletBinding(t *testing.T, repo repository.Repository, userID, chainType, wallet string) {
|
||||
t.Helper()
|
||||
|
||||
err := repo.UpsertWalletBinding(context.Background(), &model.WalletBinding{
|
||||
UserID: userID,
|
||||
ChainType: chainType,
|
||||
WalletAddress: wallet,
|
||||
Status: "ACTIVE",
|
||||
ChallengeID: "test-challenge",
|
||||
VerifiedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertWalletBinding() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanClaimRejectsExpiredAndAlreadyClaimed(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
activePacket := &model.RedPacket{
|
||||
BizID: "biz-active",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1001",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-active",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, activePacket); err != nil {
|
||||
t.Fatalf("CreateRedPacket(active) error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0xclaimer")
|
||||
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: "1001",
|
||||
ClaimerWallet: "0xclaimer",
|
||||
ClaimTxHash: "0xtx1",
|
||||
Status: "CONFIRMED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.SaveClaim(ctx, claim); err != nil {
|
||||
t.Fatalf("SaveClaim() error = %v", err)
|
||||
}
|
||||
|
||||
if err := svc.CanClaim(ctx, "1001", "0xclaimer", "u2"); err == nil || err.Error() != "already claimed" {
|
||||
t.Fatalf("expected already claimed error, got %v", err)
|
||||
}
|
||||
|
||||
expiredPacket := &model.RedPacket{
|
||||
BizID: "biz-expired",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1002",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-expired",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(-1 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, expiredPacket); err != nil {
|
||||
t.Fatalf("CreateRedPacket(expired) error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u3", "EVM", "0xfresh")
|
||||
|
||||
if err := svc.CanClaim(authctx.WithCurrentUserID(context.Background(), "u3"), "1002", "0xfresh", "u3"); err == nil || err.Error() != "packet is expired" {
|
||||
t.Fatalf("expected expired error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanClaimRejectsAlreadyClaimedByUserID(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
packet := &model.RedPacket{
|
||||
BizID: "biz-user-claimed",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1003",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-user-claimed",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, packet); err != nil {
|
||||
t.Fatalf("CreateRedPacket() error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0xanother-wallet")
|
||||
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: "1003",
|
||||
UserID: "u2",
|
||||
ClaimerWallet: "0xclaimer",
|
||||
ClaimTxHash: "0xtx-user-claimed",
|
||||
Status: "CONFIRMED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.SaveClaim(ctx, claim); err != nil {
|
||||
t.Fatalf("SaveClaim() error = %v", err)
|
||||
}
|
||||
|
||||
if err := svc.CanClaim(ctx, "1003", "0xanother-wallet", "u2"); err == nil || err.Error() != "user already claimed" {
|
||||
t.Fatalf("expected user already claimed error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanClaimUsesPacketTypeRules(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
groupPacket := &model.RedPacket{
|
||||
BizID: "biz-group",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1101",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
PacketType: 0,
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, groupPacket); err != nil {
|
||||
t.Fatalf("CreateRedPacket(group) error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0xclaimer")
|
||||
if err := svc.CanClaim(ctx, "1101", "0xclaimer", "u2"); err == nil || err.Error() != "group_id is required for fixed packet claim" {
|
||||
t.Fatalf("expected missing group_id error, got %v", err)
|
||||
}
|
||||
|
||||
transferPacket := &model.RedPacket{
|
||||
BizID: "biz-transfer",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1102",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
PacketType: 2,
|
||||
ReceiverUserID: "u-receiver",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, transferPacket); err != nil {
|
||||
t.Fatalf("CreateRedPacket(transfer) error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u-other", "EVM", "0xclaimer")
|
||||
if err := svc.CanClaim(ctx, "1102", "0xclaimer", "u-other"); err == nil || err.Error() != "user is not the designated receiver" {
|
||||
t.Fatalf("expected designated receiver error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrderPersistsScopeFields(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u-create")
|
||||
|
||||
result, err := svc.CreateOrder(ctx, &CreateOrderRequest{
|
||||
ChainType: "EVM",
|
||||
CreatorWallet: "0x1111111111111111111111111111111111111111",
|
||||
GroupID: "g-100",
|
||||
ScopeType: "group",
|
||||
PacketType: 1,
|
||||
Token: "0x2222222222222222222222222222222222222222",
|
||||
TotalAmount: "1000",
|
||||
TotalShares: 10,
|
||||
ReceiverUserIDs: []string{"u2", "u3"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOrder() error = %v", err)
|
||||
}
|
||||
|
||||
bizID, _ := result["biz_id"].(string)
|
||||
record, err := repo.GetRedPacketByBizID(ctx, bizID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRedPacketByBizID() error = %v", err)
|
||||
}
|
||||
|
||||
if record.ScopeType != "GROUP" {
|
||||
t.Fatalf("scope type mismatch: got %s", record.ScopeType)
|
||||
}
|
||||
if record.ChainType != "EVM" {
|
||||
t.Fatalf("chain type mismatch: got %s", record.ChainType)
|
||||
}
|
||||
if record.GroupID != "g-100" {
|
||||
t.Fatalf("group id mismatch: got %s", record.GroupID)
|
||||
}
|
||||
|
||||
var got []string
|
||||
if err := json.Unmarshal([]byte(record.ReceiverUserIDs), &got); err != nil {
|
||||
t.Fatalf("Unmarshal(receiver_user_ids) error = %v", err)
|
||||
}
|
||||
if len(got) != 2 || got[0] != "u2" || got[1] != "u3" {
|
||||
t.Fatalf("receiver_user_ids mismatch: got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatedCallbackUpdatesBindingAndScope(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u-create")
|
||||
|
||||
result, err := svc.CreateOrder(ctx, &CreateOrderRequest{
|
||||
ChainType: "TRON",
|
||||
CreatorWallet: "0x1111111111111111111111111111111111111111",
|
||||
PacketType: 2,
|
||||
Token: "0x0000000000000000000000000000000000000000",
|
||||
TotalAmount: "1000",
|
||||
TotalShares: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOrder() error = %v", err)
|
||||
}
|
||||
|
||||
bizID, _ := result["biz_id"].(string)
|
||||
err = svc.CreatedCallback(ctx, &CreatedCallbackRequest{
|
||||
BizID: bizID,
|
||||
TxHash: "0xabc123",
|
||||
PacketID: "3001",
|
||||
ScopeType: "DIRECT",
|
||||
ReceiverUserID: "u-receiver",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatedCallback() error = %v", err)
|
||||
}
|
||||
|
||||
record, err := repo.GetRedPacketByBizID(ctx, bizID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRedPacketByBizID() error = %v", err)
|
||||
}
|
||||
|
||||
if record.PacketID != "3001" {
|
||||
t.Fatalf("packet id mismatch: got %s", record.PacketID)
|
||||
}
|
||||
if record.ChainType != "TRON" {
|
||||
t.Fatalf("chain type mismatch: got %s", record.ChainType)
|
||||
}
|
||||
if record.TxHash != "0xabc123" {
|
||||
t.Fatalf("tx hash mismatch: got %s", record.TxHash)
|
||||
}
|
||||
if record.Status != "ACTIVE" {
|
||||
t.Fatalf("status mismatch: got %s", record.Status)
|
||||
}
|
||||
if record.ScopeType != "DIRECT" {
|
||||
t.Fatalf("scope type mismatch: got %s", record.ScopeType)
|
||||
}
|
||||
if record.ReceiverUserID != "u-receiver" {
|
||||
t.Fatalf("receiver user mismatch: got %s", record.ReceiverUserID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueClaimSignValidatesInputsAndPersistsAuth(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
packet := &model.RedPacket{
|
||||
BizID: "biz-sign",
|
||||
ChainType: "EVM",
|
||||
PacketID: "2001",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-sign",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, packet); err != nil {
|
||||
t.Fatalf("CreateRedPacket() error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0x1111111111111111111111111111111111111111")
|
||||
|
||||
if _, err := svc.IssueClaimSign(ctx, "bad-packet-id", "0x1111111111111111111111111111111111111111", "0"); err == nil {
|
||||
t.Fatalf("expected invalid packet id error")
|
||||
}
|
||||
|
||||
result, err := svc.IssueClaimSign(ctx, "2001", "0x1111111111111111111111111111111111111111", "123")
|
||||
if err != nil {
|
||||
t.Fatalf("IssueClaimSign() error = %v", err)
|
||||
}
|
||||
|
||||
auth, err := repo.GetClaimAuth(ctx, "2001", "0x1111111111111111111111111111111111111111")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClaimAuth() error = %v", err)
|
||||
}
|
||||
if auth.AuthNonce == "" {
|
||||
t.Fatalf("expected auth nonce to be persisted")
|
||||
}
|
||||
if auth.RandomSeed != "123" {
|
||||
t.Fatalf("random seed mismatch: got %s", auth.RandomSeed)
|
||||
}
|
||||
if result["auth_nonce"] == "" {
|
||||
t.Fatalf("expected auth_nonce in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimResultPersistsPendingWithoutChainParser(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
packet := &model.RedPacket{
|
||||
BizID: "biz-claim-result",
|
||||
ChainType: "EVM",
|
||||
PacketID: "2101",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-1",
|
||||
PacketType: 0,
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, packet); err != nil {
|
||||
t.Fatalf("CreateRedPacket() error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0x1111111111111111111111111111111111111111")
|
||||
|
||||
err := svc.ClaimResult(ctx, &ClaimResultRequest{
|
||||
PacketID: "2101",
|
||||
Claimer: "0x1111111111111111111111111111111111111111",
|
||||
TxHash: "0xtx-claim",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimResult() error = %v", err)
|
||||
}
|
||||
|
||||
claim, err := repo.GetClaimByPacketIDAndClaimer(ctx, "2101", "0x1111111111111111111111111111111111111111")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClaimByPacketIDAndClaimer() error = %v", err)
|
||||
}
|
||||
if claim.Status != "PENDING" {
|
||||
t.Fatalf("claim status mismatch: got %s", claim.Status)
|
||||
}
|
||||
if claim.UserID != "u2" {
|
||||
t.Fatalf("user id mismatch: got %s", claim.UserID)
|
||||
}
|
||||
}
|
||||
@ -1,163 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"redpacket/config"
|
||||
"redpacket/internal/chain"
|
||||
"redpacket/internal/handler"
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
"redpacket/internal/service"
|
||||
"redpacket/router"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/cmd"
|
||||
"github.com/openimsdk/tools/system/program"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
cfgFile := ""
|
||||
if len(os.Args) > 1 {
|
||||
cfgFile = os.Args[1]
|
||||
}
|
||||
config.Load(cfgFile)
|
||||
cfg := &config.Cfg
|
||||
|
||||
// Connect to database
|
||||
db, err := openDB(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Auto-migrate models
|
||||
if err := db.AutoMigrate(
|
||||
&model.RedPacket{},
|
||||
&model.RedPacketClaim{},
|
||||
&model.RedPacketClaimAuth{},
|
||||
&model.RedPacketRefund{},
|
||||
&model.WalletBindingChallenge{},
|
||||
&model.WalletBinding{},
|
||||
); err != nil {
|
||||
log.Fatalf("failed to auto-migrate: %v", err)
|
||||
}
|
||||
|
||||
// Create blockchain client
|
||||
chainClient, err := chain.NewClient(
|
||||
cfg.Chain.RPCURL,
|
||||
cfg.Chain.ContractAddress,
|
||||
cfg.Chain.ChainID,
|
||||
cfg.Chain.SignerPrivateKey,
|
||||
cfg.Chain.ConfigAdminPrivateKey,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to create chain client: %v (continuing with mock mode)", err)
|
||||
// Continue without blockchain for now - can be configured later
|
||||
}
|
||||
|
||||
// Create TRON client if configured
|
||||
var tronClient *chain.TronClient
|
||||
if cfg.Tron.FullNodeURL != "" {
|
||||
abiJSON, err := chain.ExtractABIFromEmbeddedArtifact()
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to load ABI for TRON: %v", err)
|
||||
} else {
|
||||
tronClient, err = chain.NewTronClient(
|
||||
cfg.Tron.FullNodeURL,
|
||||
cfg.Tron.ContractBase58,
|
||||
cfg.Tron.OwnerBase58,
|
||||
cfg.Tron.PrivateKeyHex,
|
||||
abiJSON,
|
||||
cfg.Tron.FeeLimit,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to create TRON client: %v", err)
|
||||
tronClient = nil
|
||||
} else {
|
||||
log.Println("✅ TRON client initialized successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create repository and service
|
||||
repo := repository.New(db)
|
||||
rpSvc := service.NewRedPacketService(repo, chainClient, tronClient, cfg.Chain.SignerPrivateKey)
|
||||
|
||||
// Create admin service and handler
|
||||
adminSvc := service.NewAdminService(chainClient, tronClient)
|
||||
adminHandler := handler.NewAdminHandler(adminSvc)
|
||||
|
||||
// Create user handler
|
||||
rpHandler := handler.NewRedPacketHandler(rpSvc)
|
||||
|
||||
// Setup router
|
||||
r := gin.Default()
|
||||
router.Setup(r, rpHandler, adminHandler)
|
||||
|
||||
// Start blockchain indexers
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// ETH Indexer
|
||||
if chainClient != nil {
|
||||
ethIndexer := chain.NewIndexer(chainClient, repo, cfg.Indexer.PollInterval, 0)
|
||||
ethIndexer.Start(ctx)
|
||||
log.Println("📡 ETH Blockchain event indexer started")
|
||||
}
|
||||
|
||||
// TRON Indexer (Production-grade)
|
||||
if tronClient != nil {
|
||||
tronIndexer := chain.NewTronIndexer(tronClient, repo, cfg.Indexer.PollInterval, 0)
|
||||
tronIndexer.Start(ctx)
|
||||
log.Println("📡 TRON Blockchain event indexer started (Production mode)")
|
||||
}
|
||||
|
||||
// Start HTTP server with graceful shutdown
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("🚀 RedPacket service listening on :%d", cfg.Server.Port)
|
||||
log.Printf("📋 Health check: http://localhost:%d/health", cfg.Server.Port)
|
||||
log.Printf("📋 API docs: see backend-api.md")
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("listen: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("shutting down server...")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Printf("server forced shutdown: %v", err)
|
||||
}
|
||||
log.Println("server stopped")
|
||||
}
|
||||
|
||||
func openDB(cfg *config.Config) (*gorm.DB, error) {
|
||||
switch cfg.DB.Driver {
|
||||
case "mysql":
|
||||
return gorm.Open(mysql.Open(cfg.DB.DSN), &gorm.Config{})
|
||||
case "sqlite", "":
|
||||
return gorm.Open(sqlite.Open(cfg.DB.DSN), &gorm.Config{})
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported db.driver: %s", cfg.DB.Driver)
|
||||
if err := cmd.NewRedPacketRpcCmd().Exec(); err != nil {
|
||||
program.ExitWithError(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
package resp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func OK(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func Fail(c *gin.Context, httpCode, code int, message string) {
|
||||
c.JSON(httpCode, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusBadRequest, 400, message)
|
||||
}
|
||||
|
||||
func Forbidden(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusForbidden, 403, message)
|
||||
}
|
||||
|
||||
func InternalError(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusInternalServerError, 500, message)
|
||||
}
|
||||
Binary file not shown.
@ -1,37 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"redpacket/internal/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Setup(r *gin.Engine, rpHandler *handler.RedPacketHandler, adminHandler *handler.AdminHandler) {
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// User-facing red packet APIs
|
||||
api := r.Group("/api/redpacket")
|
||||
{
|
||||
api.POST("/create-order", rpHandler.CreateOrder)
|
||||
api.POST("/created-callback", rpHandler.CreatedCallback)
|
||||
api.GET("/detail", rpHandler.Detail)
|
||||
api.POST("/claim-sign", rpHandler.ClaimSign)
|
||||
api.POST("/claim-result", rpHandler.ClaimResult)
|
||||
api.POST("/wallet-bind/challenge", rpHandler.WalletBindChallenge)
|
||||
api.POST("/wallet-bind/confirm", rpHandler.WalletBindConfirm)
|
||||
api.GET("/wallet-bind/detail", rpHandler.WalletBindDetail)
|
||||
}
|
||||
|
||||
// Admin APIs - should be protected with authentication in production
|
||||
admin := r.Group("/admin/redpacket")
|
||||
{
|
||||
admin.POST("/set-signer", adminHandler.SetSigner)
|
||||
admin.POST("/set-token", adminHandler.SetToken)
|
||||
admin.POST("/set-expiry", adminHandler.SetExpiry)
|
||||
admin.POST("/set-allow-all-tokens", adminHandler.SetAllowAllTokens)
|
||||
admin.POST("/set-native-token", adminHandler.SetNativeTokenEnabled)
|
||||
admin.POST("/parse-tx-events", adminHandler.ParseTxEvents)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
rpc:
|
||||
registerIP: ""
|
||||
listenIP: 0.0.0.0
|
||||
autoSetPorts: false
|
||||
ports: [10560]
|
||||
|
||||
prometheus:
|
||||
enable: false
|
||||
ports: [12560]
|
||||
|
||||
# EVM (Ethereum / Polygon / BSC / ...) chain configuration.
|
||||
# Leave rpcURL empty to disable the EVM client; the RPC service will then
|
||||
# only expose TRON-related functionality (or the offchain code paths).
|
||||
chain:
|
||||
rpcURL: ""
|
||||
contractAddress: ""
|
||||
chainID: 0
|
||||
signerPrivateKey: ""
|
||||
configAdminPrivateKey: ""
|
||||
|
||||
# TRON full-node configuration. Leave fullNodeURL empty to disable TRON.
|
||||
tron:
|
||||
fullNodeURL: ""
|
||||
contractBase58: ""
|
||||
ownerBase58: ""
|
||||
privateKeyHex: ""
|
||||
feeLimit: 100000000
|
||||
|
||||
# Indexer polling interval (in seconds). Used by both EVM and TRON event indexers.
|
||||
indexer:
|
||||
pollInterval: 5
|
||||
@ -0,0 +1,217 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
pbredpacket "github.com/openimsdk/protocol/redpacket"
|
||||
"github.com/openimsdk/tools/a2r"
|
||||
"github.com/openimsdk/tools/apiresp"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
type RedPacketApi struct {
|
||||
Client pbredpacket.RedPacketClient
|
||||
}
|
||||
|
||||
func NewRedPacketApi(client pbredpacket.RedPacketClient) *RedPacketApi {
|
||||
return &RedPacketApi{Client: client}
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) CreateOrder(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.CreateOrderReq](ctx)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "redpacket create order parse failed", err)
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.CreateOrder(ctx, req)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "redpacket create order rpc failed", err)
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) CreatedCallback(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.CreatedCallbackReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.CreatedCallback(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) GetDetail(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.GetDetailReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.GetDetail(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) IssueClaimSign(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.IssueClaimSignReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.IssueClaimSign(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) ClaimResult(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.ClaimResultReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.ClaimResult(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) IssueWalletBindChallenge(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.IssueWalletBindChallengeReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.IssueWalletBindChallenge(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) ConfirmWalletBind(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.ConfirmWalletBindReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.ConfirmWalletBind(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) GetWalletBinding(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.GetWalletBindingReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.GetWalletBinding(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
// Admin endpoints
|
||||
|
||||
func (h *RedPacketApi) AdminSetSigner(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetSignerReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.SetSigner(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) AdminSetToken(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetTokenReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.SetToken(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) AdminSetExpiry(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetExpiryReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.SetExpiry(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) AdminSetAllowAllTokens(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetAllowAllTokensReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.SetAllowAllTokens(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) AdminSetNativeTokenEnabled(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetNativeTokenEnabledReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.SetNativeTokenEnabled(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (h *RedPacketApi) AdminParseTxEvents(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbredpacket.ParseTxEventsReq](ctx)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.Client.ParseTxEvents(ctx, req)
|
||||
if err != nil {
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
pbredpacket "github.com/openimsdk/protocol/redpacket"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
func (s *redPacketServer) SetSigner(ctx context.Context, req *pbredpacket.SetSignerReq) (*pbredpacket.SetSignerResp, error) {
|
||||
if req.SignerAddress == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("signer_address is required")
|
||||
}
|
||||
if s.chainClient != nil {
|
||||
log.ZInfo(ctx, "redpacket admin setSigner (eth mock)", "signerAddress", req.SignerAddress)
|
||||
return &pbredpacket.SetSignerResp{Message: "signer address updated successfully"}, nil
|
||||
}
|
||||
if s.tronClient != nil {
|
||||
if _, err := s.tronClient.SendAdminTransaction(ctx, "setSigner", req.SignerAddress); err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("setSigner failed: " + err.Error())
|
||||
}
|
||||
return &pbredpacket.SetSignerResp{Message: "signer address updated successfully"}, nil
|
||||
}
|
||||
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *redPacketServer) SetToken(ctx context.Context, req *pbredpacket.SetTokenReq) (*pbredpacket.SetTokenResp, error) {
|
||||
if req.TokenAddress == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("token_address is required")
|
||||
}
|
||||
|
||||
minAmountBig := new(big.Int)
|
||||
if req.MinAmount != "" {
|
||||
minAmountBig.SetString(req.MinAmount, 10)
|
||||
}
|
||||
|
||||
if s.chainClient != nil {
|
||||
log.ZInfo(ctx, "redpacket admin setToken (eth mock)",
|
||||
"tokenAddress", req.TokenAddress,
|
||||
"allowed", req.Allowed,
|
||||
"minAmount", req.MinAmount,
|
||||
)
|
||||
return &pbredpacket.SetTokenResp{Message: "token configuration updated"}, nil
|
||||
}
|
||||
if s.tronClient != nil {
|
||||
if _, err := s.tronClient.SendAdminTransaction(ctx, "setAllowedToken", req.TokenAddress, req.Allowed, minAmountBig); err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("setAllowedToken failed: " + err.Error())
|
||||
}
|
||||
return &pbredpacket.SetTokenResp{Message: "token configuration updated"}, nil
|
||||
}
|
||||
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *redPacketServer) SetExpiry(ctx context.Context, req *pbredpacket.SetExpiryReq) (*pbredpacket.SetExpiryResp, error) {
|
||||
if req.ExpirySeconds <= 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("expiry_seconds must be positive")
|
||||
}
|
||||
if s.chainClient != nil {
|
||||
log.ZInfo(ctx, "redpacket admin setExpiry (eth mock)", "expirySeconds", req.ExpirySeconds)
|
||||
return &pbredpacket.SetExpiryResp{Message: "expiry duration updated"}, nil
|
||||
}
|
||||
if s.tronClient != nil {
|
||||
if _, err := s.tronClient.SendAdminTransaction(ctx, "setDefaultExpiryDuration", req.ExpirySeconds); err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("setDefaultExpiryDuration failed: " + err.Error())
|
||||
}
|
||||
return &pbredpacket.SetExpiryResp{Message: "expiry duration updated"}, nil
|
||||
}
|
||||
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *redPacketServer) SetAllowAllTokens(ctx context.Context, req *pbredpacket.SetAllowAllTokensReq) (*pbredpacket.SetAllowAllTokensResp, error) {
|
||||
if s.chainClient != nil {
|
||||
log.ZInfo(ctx, "redpacket admin setAllowAllTokens (eth mock)", "allowAll", req.AllowAll)
|
||||
return &pbredpacket.SetAllowAllTokensResp{Message: "allow all tokens setting updated"}, nil
|
||||
}
|
||||
if s.tronClient != nil {
|
||||
if _, err := s.tronClient.SendAdminTransaction(ctx, "setAllowAllTokens", req.AllowAll); err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("setAllowAllTokens failed: " + err.Error())
|
||||
}
|
||||
return &pbredpacket.SetAllowAllTokensResp{Message: "allow all tokens setting updated"}, nil
|
||||
}
|
||||
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *redPacketServer) SetNativeTokenEnabled(ctx context.Context, req *pbredpacket.SetNativeTokenEnabledReq) (*pbredpacket.SetNativeTokenEnabledResp, error) {
|
||||
if s.chainClient != nil {
|
||||
log.ZInfo(ctx, "redpacket admin setNativeTokenEnabled (eth mock)", "enabled", req.Enabled)
|
||||
return &pbredpacket.SetNativeTokenEnabledResp{Message: "native token setting updated"}, nil
|
||||
}
|
||||
if s.tronClient != nil {
|
||||
if _, err := s.tronClient.SendAdminTransaction(ctx, "setNativeTokenEnabled", req.Enabled); err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("setNativeTokenEnabled failed: " + err.Error())
|
||||
}
|
||||
return &pbredpacket.SetNativeTokenEnabledResp{Message: "native token setting updated"}, nil
|
||||
}
|
||||
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *redPacketServer) ParseTxEvents(ctx context.Context, req *pbredpacket.ParseTxEventsReq) (*pbredpacket.ParseTxEventsResp, error) {
|
||||
if req.TxHash == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("tx_hash is required")
|
||||
}
|
||||
|
||||
if req.Chain == "tron" && s.tronClient != nil {
|
||||
return &pbredpacket.ParseTxEventsResp{
|
||||
Chain: "tron",
|
||||
TxHash: req.TxHash,
|
||||
Note: "TRON event parsing not fully implemented in this version",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if s.chainClient != nil {
|
||||
txHashBytes := common.HexToHash(req.TxHash)
|
||||
events, err := s.chainClient.ParseTransactionReceipt(ctx, txHashBytes)
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("parse tx receipt failed: " + err.Error())
|
||||
}
|
||||
|
||||
out := make([]*pbredpacket.ParsedEvent, 0, len(events))
|
||||
for _, e := range events {
|
||||
data := make(map[string]string, len(e.Data))
|
||||
for k, v := range e.Data {
|
||||
data[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
out = append(out, &pbredpacket.ParsedEvent{
|
||||
Name: e.Name,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
return &pbredpacket.ParseTxEventsResp{
|
||||
Chain: "eth",
|
||||
TxHash: req.TxHash,
|
||||
Events: out,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errs.ErrInternalServer.WrapMsg("no client available for chain: " + req.Chain)
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/openimsdk/open-im-server/v3/internal/rpc/redpacket/chain"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
|
||||
pbredpacket "github.com/openimsdk/protocol/redpacket"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.RedPacket
|
||||
MongodbConfig config.Mongo
|
||||
Share config.Share
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
type redPacketServer struct {
|
||||
pbredpacket.UnimplementedRedPacketServer
|
||||
config *Config
|
||||
db controller.RedPacketDatabase
|
||||
chainClient *chain.ChainClient
|
||||
tronClient *chain.TronClient
|
||||
signerKey *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, conf *Config, _ discovery.SvcDiscoveryRegistry, server *grpc.Server) error {
|
||||
mgoClient, err := mongoutil.NewMongoDB(ctx, conf.MongodbConfig.Build())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db := mgoClient.GetDB()
|
||||
|
||||
rpDB, err := mgo.NewRedPacketMongo(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
claimDB, err := mgo.NewRedPacketClaimMongo(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
claimAuthDB, err := mgo.NewRedPacketClaimAuthMongo(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refundDB, err := mgo.NewRedPacketRefundMongo(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
challengeDB, err := mgo.NewWalletBindingChallengeMongo(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bindingDB, err := mgo.NewWalletBindingMongo(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo := controller.NewRedPacketDatabase(rpDB, claimDB, claimAuthDB, refundDB, challengeDB, bindingDB)
|
||||
|
||||
chainClient, err := chain.NewClient(
|
||||
conf.RpcConfig.Chain.RPCURL,
|
||||
conf.RpcConfig.Chain.ContractAddress,
|
||||
conf.RpcConfig.Chain.ChainID,
|
||||
conf.RpcConfig.Chain.SignerPrivateKey,
|
||||
conf.RpcConfig.Chain.ConfigAdminPrivateKey,
|
||||
)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "redpacket eth client init failed, continuing without it", err)
|
||||
chainClient = nil
|
||||
}
|
||||
|
||||
var tronClient *chain.TronClient
|
||||
if conf.RpcConfig.Tron.FullNodeURL != "" {
|
||||
abiJSON, abiErr := chain.ExtractABIFromEmbeddedArtifact()
|
||||
if abiErr != nil {
|
||||
log.ZWarn(ctx, "redpacket tron load abi failed", abiErr)
|
||||
} else {
|
||||
tronClient, err = chain.NewTronClient(
|
||||
conf.RpcConfig.Tron.FullNodeURL,
|
||||
conf.RpcConfig.Tron.ContractBase58,
|
||||
conf.RpcConfig.Tron.OwnerBase58,
|
||||
conf.RpcConfig.Tron.PrivateKeyHex,
|
||||
abiJSON,
|
||||
conf.RpcConfig.Tron.FeeLimit,
|
||||
)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "redpacket tron client init failed", err)
|
||||
tronClient = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var signerKey *ecdsa.PrivateKey
|
||||
if k := conf.RpcConfig.Chain.SignerPrivateKey; k != "" {
|
||||
sk, parseErr := crypto.HexToECDSA(k)
|
||||
if parseErr != nil {
|
||||
log.ZWarn(ctx, "redpacket signer private key parse failed", parseErr)
|
||||
} else {
|
||||
signerKey = sk
|
||||
}
|
||||
}
|
||||
|
||||
srv := &redPacketServer{
|
||||
config: conf,
|
||||
db: repo,
|
||||
chainClient: chainClient,
|
||||
tronClient: tronClient,
|
||||
signerKey: signerKey,
|
||||
}
|
||||
|
||||
pbredpacket.RegisterRedPacketServer(server, srv)
|
||||
|
||||
if chainClient != nil {
|
||||
ethIndexer := chain.NewIndexer(chainClient, repo, conf.RpcConfig.Indexer.PollInterval, 0)
|
||||
ethIndexer.Start(ctx)
|
||||
}
|
||||
if tronClient != nil {
|
||||
tronIndexer := chain.NewTronIndexer(tronClient, repo, conf.RpcConfig.Indexer.PollInterval, 0)
|
||||
tronIndexer.Start(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,777 @@
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/google/uuid"
|
||||
"github.com/openimsdk/open-im-server/v3/internal/rpc/redpacket/chain"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
pbredpacket "github.com/openimsdk/protocol/redpacket"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
func (s *redPacketServer) CreateOrder(ctx context.Context, req *pbredpacket.CreateOrderReq) (*pbredpacket.CreateOrderResp, error) {
|
||||
currentUserID := mcontext.GetOpUserID(ctx)
|
||||
if currentUserID == "" {
|
||||
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
|
||||
}
|
||||
|
||||
bizID := uuid.NewString()
|
||||
chainType, err := normalizeChainType(req.ChainType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scopeType := normalizeScopeType(req.ScopeType)
|
||||
if err := validateCreateScope(scopeType, req.GroupID, req.ReceiverUserID, req.ReceiverUserIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.validateCreateHook(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chainID := req.ChainID
|
||||
contractAddress := strings.TrimSpace(req.ContractAddress)
|
||||
if chainType == "EVM" && s.chainClient != nil {
|
||||
if chainID == 0 {
|
||||
if chainValue := s.chainClient.ChainID(); chainValue != nil {
|
||||
chainID = chainValue.Int64()
|
||||
}
|
||||
}
|
||||
if contractAddress == "" {
|
||||
contractAddress = s.chainClient.ContractAddress().Hex()
|
||||
}
|
||||
}
|
||||
if chainType == "TRON" && s.tronClient != nil && contractAddress == "" {
|
||||
contractAddress = s.tronClient.ContractAddress()
|
||||
}
|
||||
|
||||
rp := &model.RedPacket{
|
||||
BizID: bizID,
|
||||
ChainType: chainType,
|
||||
ChainID: chainID,
|
||||
ContractAddress: contractAddress,
|
||||
CreatorUserID: currentUserID,
|
||||
CreatorWallet: req.CreatorWallet,
|
||||
GroupID: req.GroupID,
|
||||
ScopeType: scopeType,
|
||||
ReceiverUserID: req.ReceiverUserID,
|
||||
ReceiverUserIDs: append([]string(nil), req.ReceiverUserIDs...),
|
||||
PacketType: req.PacketType,
|
||||
Token: req.Token,
|
||||
TotalAmount: req.TotalAmount,
|
||||
TotalShares: req.TotalShares,
|
||||
ExpiryAt: req.ExpiryAt,
|
||||
Status: "PENDING",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.db.CreateRedPacket(ctx, rp); err != nil {
|
||||
log.ZError(ctx, "create redpacket failed", err, "bizID", bizID)
|
||||
return nil, servererrs.ErrDatabase.WrapMsg("failed to create red packet")
|
||||
}
|
||||
|
||||
return &pbredpacket.CreateOrderResp{BizID: bizID}, nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) CreatedCallback(ctx context.Context, req *pbredpacket.CreatedCallbackReq) (*pbredpacket.CreatedCallbackResp, error) {
|
||||
if strings.TrimSpace(req.BizID) == "" || strings.TrimSpace(req.TxHash) == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("biz_id and tx_hash are required")
|
||||
}
|
||||
|
||||
rp, err := s.db.GetRedPacketByBizID(ctx, req.BizID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupID := firstNonEmpty(req.GroupID, rp.GroupID)
|
||||
scopeType := normalizeScopeType(firstNonEmpty(req.ScopeType, rp.ScopeType))
|
||||
receiverUserID := firstNonEmpty(req.ReceiverUserID, rp.ReceiverUserID)
|
||||
receiverUserIDs := rp.ReceiverUserIDs
|
||||
if len(req.ReceiverUserIDs) > 0 {
|
||||
receiverUserIDs = append([]string(nil), req.ReceiverUserIDs...)
|
||||
}
|
||||
|
||||
if err := validateCreateScope(scopeType, groupID, receiverUserID, receiverUserIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createdPacket, err := s.resolveCreatedPacket(ctx, rp, req.TxHash, req.PacketID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.db.UpdateRedPacketCreated(ctx, &model.RedPacket{
|
||||
BizID: req.BizID,
|
||||
ChainType: rp.ChainType,
|
||||
PacketID: createdPacket.PacketID,
|
||||
ChainID: createdPacket.ChainID,
|
||||
ContractAddress: createdPacket.ContractAddress,
|
||||
TxHash: req.TxHash,
|
||||
GroupID: groupID,
|
||||
ScopeType: scopeType,
|
||||
ReceiverUserID: receiverUserID,
|
||||
ReceiverUserIDs: receiverUserIDs,
|
||||
Status: "ACTIVE",
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbredpacket.CreatedCallbackResp{}, nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) GetDetail(ctx context.Context, req *pbredpacket.GetDetailReq) (*pbredpacket.GetDetailResp, error) {
|
||||
if strings.TrimSpace(req.PacketID) == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("packet_id is required")
|
||||
}
|
||||
|
||||
rp, err := s.db.GetRedPacketByPacketID(ctx, req.PacketID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, err := s.db.GetClaimsByPacketID(ctx, req.PacketID)
|
||||
if err != nil {
|
||||
claims = nil
|
||||
}
|
||||
|
||||
return &pbredpacket.GetDetailResp{
|
||||
Record: redPacketModelToProto(rp),
|
||||
Claims: claimsModelToProto(claims),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) IssueClaimSign(ctx context.Context, req *pbredpacket.IssueClaimSignReq) (*pbredpacket.IssueClaimSignResp, error) {
|
||||
currentUserID := mcontext.GetOpUserID(ctx)
|
||||
if currentUserID == "" {
|
||||
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
|
||||
}
|
||||
if strings.TrimSpace(req.PacketID) == "" || strings.TrimSpace(req.Claimer) == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("packet_id and claimer are required")
|
||||
}
|
||||
if err := s.canClaim(ctx, req.PacketID, req.Claimer, currentUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packetIDBig := new(big.Int)
|
||||
if _, ok := packetIDBig.SetString(req.PacketID, 10); !ok {
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid packet_id", "packetID", req.PacketID)
|
||||
}
|
||||
|
||||
claimerAddr := common.HexToAddress(req.Claimer)
|
||||
nonce := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
authNonceBig := new(big.Int)
|
||||
authNonceBig.SetString(nonce, 10)
|
||||
deadline := time.Now().Add(5 * time.Minute).Unix()
|
||||
randomSeedBig := new(big.Int)
|
||||
if req.RandomSeed != "" && req.RandomSeed != "0" {
|
||||
if _, ok := randomSeedBig.SetString(req.RandomSeed, 10); !ok {
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid random_seed", "randomSeed", req.RandomSeed)
|
||||
}
|
||||
} else {
|
||||
randomSeedBig.SetInt64(time.Now().UnixNano())
|
||||
}
|
||||
deadlineBig := big.NewInt(deadline)
|
||||
|
||||
var digest [32]byte
|
||||
var err error
|
||||
if s.chainClient != nil {
|
||||
digest, err = s.chainClient.GetSignMessage(ctx, packetIDBig, claimerAddr, authNonceBig, randomSeedBig, deadlineBig)
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("getSignMessage failed: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
digest = crypto.Keccak256Hash([]byte(fmt.Sprintf("%s:%s:%s:%s:%d", req.PacketID, req.Claimer, nonce, randomSeedBig.String(), deadline)))
|
||||
}
|
||||
|
||||
var signature []byte
|
||||
if s.signerKey != nil {
|
||||
signature, err = crypto.Sign(digest[:], s.signerKey)
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("sign failed: " + err.Error())
|
||||
}
|
||||
if len(signature) == 65 && signature[64] < 27 {
|
||||
signature[64] += 27
|
||||
}
|
||||
} else {
|
||||
signature = []byte("0xplaceholder-signature-for-testing")
|
||||
}
|
||||
|
||||
sigHex := "0x" + hex.EncodeToString(signature)
|
||||
|
||||
auth := &model.RedPacketClaimAuth{
|
||||
PacketID: req.PacketID,
|
||||
Claimer: req.Claimer,
|
||||
AuthNonce: nonce,
|
||||
RandomSeed: randomSeedBig.String(),
|
||||
Deadline: deadline,
|
||||
Signature: sigHex,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.db.CreateClaimAuth(ctx, auth); err != nil {
|
||||
return nil, servererrs.ErrDatabase.WrapMsg("save claim auth failed: " + err.Error())
|
||||
}
|
||||
|
||||
return &pbredpacket.IssueClaimSignResp{
|
||||
AuthNonce: nonce,
|
||||
Deadline: deadline,
|
||||
Signature: sigHex,
|
||||
RandomSeed: randomSeedBig.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) ClaimResult(ctx context.Context, req *pbredpacket.ClaimResultReq) (*pbredpacket.ClaimResultResp, error) {
|
||||
currentUserID := mcontext.GetOpUserID(ctx)
|
||||
if currentUserID == "" {
|
||||
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
|
||||
}
|
||||
if strings.TrimSpace(req.PacketID) == "" || strings.TrimSpace(req.Claimer) == "" || strings.TrimSpace(req.TxHash) == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("packet_id, claimer and tx_hash are required")
|
||||
}
|
||||
|
||||
rp, err := s.db.GetRedPacketByPacketID(ctx, req.PacketID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateClaimBase(rp, currentUserID, req.Claimer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: req.PacketID,
|
||||
UserID: currentUserID,
|
||||
ClaimerWallet: req.Claimer,
|
||||
ClaimTxHash: req.TxHash,
|
||||
Status: "PENDING",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.db.SaveClaim(ctx, claim); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claimedEvent, err := s.resolveClaimedEvent(ctx, rp, req.TxHash)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "resolve claim event failed", err, "txHash", req.TxHash)
|
||||
return &pbredpacket.ClaimResultResp{}, nil
|
||||
}
|
||||
if claimedEvent == nil {
|
||||
return &pbredpacket.ClaimResultResp{}, nil
|
||||
}
|
||||
if !strings.EqualFold(claimedEvent.ClaimerWallet, req.Claimer) {
|
||||
return nil, errs.ErrArgs.WrapMsg(fmt.Sprintf("claim event claimer mismatch: got %s want %s", claimedEvent.ClaimerWallet, req.Claimer))
|
||||
}
|
||||
|
||||
confirmed := &model.RedPacketClaim{
|
||||
PacketID: req.PacketID,
|
||||
UserID: currentUserID,
|
||||
ClaimerWallet: claimedEvent.ClaimerWallet,
|
||||
AuthNonce: claimedEvent.AuthNonce,
|
||||
ClaimTxHash: req.TxHash,
|
||||
ClaimedAmount: claimedEvent.Amount,
|
||||
BlockNumber: claimedEvent.BlockNumber,
|
||||
Status: "CONFIRMED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := s.db.SaveClaim(ctx, confirmed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claimedEvent.AuthNonce != "" {
|
||||
if err := s.db.MarkClaimAuthUsed(ctx, claimedEvent.AuthNonce); err != nil {
|
||||
log.ZWarn(ctx, "mark claim auth used failed", err, "authNonce", claimedEvent.AuthNonce)
|
||||
}
|
||||
}
|
||||
|
||||
nextStatus := derivePacketStatusAfterClaim(rp, claimedEvent.Amount)
|
||||
if err := s.db.UpdateRedPacketClaimProgress(ctx, req.PacketID, claimedEvent.Amount, nextStatus); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbredpacket.ClaimResultResp{}, nil
|
||||
}
|
||||
|
||||
// canClaim runs the claim-eligibility check (formerly RedPacketService.CanClaim).
|
||||
func (s *redPacketServer) canClaim(ctx context.Context, packetID, claimer, userID string) error {
|
||||
rp, err := s.db.GetRedPacketByPacketID(ctx, packetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateClaimBase(rp, userID, claimer); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.ensureWalletBinding(ctx, userID, claimer, rp.ChainType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch rp.PacketType {
|
||||
case 0:
|
||||
return s.validateFixedPacketClaim(ctx, rp, userID, claimer)
|
||||
case 1:
|
||||
return s.validateRandomPacketClaim(ctx, rp, userID, claimer)
|
||||
case 2:
|
||||
return s.validateTransferPacketClaim(ctx, rp, userID, claimer)
|
||||
default:
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("unsupported packet_type: %d", rp.PacketType))
|
||||
}
|
||||
}
|
||||
|
||||
type claimedEventSnapshot struct {
|
||||
ClaimerWallet string
|
||||
AuthNonce string
|
||||
Amount string
|
||||
BlockNumber uint64
|
||||
}
|
||||
|
||||
type createdPacketSnapshot struct {
|
||||
PacketID string
|
||||
ChainID int64
|
||||
ContractAddress string
|
||||
CreatorWallet string
|
||||
PacketType int32
|
||||
Token string
|
||||
TotalAmount string
|
||||
TotalShares int32
|
||||
ExpiryAt int64
|
||||
}
|
||||
|
||||
func (s *redPacketServer) resolveCreatedPacket(ctx context.Context, rp *model.RedPacket, txHashHex, fallbackPacketID string) (*createdPacketSnapshot, error) {
|
||||
switch rp.ChainType {
|
||||
case "EVM":
|
||||
if s.chainClient == nil {
|
||||
if fallbackPacketID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("packet_id is required when EVM client is unavailable")
|
||||
}
|
||||
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
|
||||
}
|
||||
|
||||
events, err := s.chainClient.ParseTransactionReceipt(ctx, common.HexToHash(txHashHex))
|
||||
if err != nil {
|
||||
if fallbackPacketID == "" {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("parse created tx failed: " + err.Error())
|
||||
}
|
||||
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if event.Name != "PacketCreated" {
|
||||
continue
|
||||
}
|
||||
createdPacket := buildCreatedPacketSnapshot(rp, event)
|
||||
if chainValue := s.chainClient.ChainID(); chainValue != nil {
|
||||
createdPacket.ChainID = chainValue.Int64()
|
||||
}
|
||||
createdPacket.ContractAddress = s.chainClient.ContractAddress().Hex()
|
||||
if err := validateCreatedPacket(rp, createdPacket); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createdPacket, nil
|
||||
}
|
||||
|
||||
if fallbackPacketID == "" {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("PacketCreated event not found in tx: " + txHashHex)
|
||||
}
|
||||
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
|
||||
case "TRON":
|
||||
if s.tronClient == nil {
|
||||
if fallbackPacketID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("packet_id is required when TRON client is unavailable")
|
||||
}
|
||||
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
|
||||
}
|
||||
|
||||
events, err := s.tronClient.ParseTransactionReceipt(ctx, txHashHex)
|
||||
if err != nil {
|
||||
if fallbackPacketID == "" {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("parse tron created tx failed: " + err.Error())
|
||||
}
|
||||
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if event.Name != "PacketCreated" {
|
||||
continue
|
||||
}
|
||||
createdPacket := buildCreatedPacketSnapshot(rp, event)
|
||||
createdPacket.ContractAddress = firstNonEmpty(s.tronClient.ContractAddress(), rp.ContractAddress)
|
||||
if err := validateCreatedPacket(rp, createdPacket); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createdPacket, nil
|
||||
}
|
||||
|
||||
if fallbackPacketID == "" {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("PacketCreated event not found in TRON tx: " + txHashHex)
|
||||
}
|
||||
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
|
||||
default:
|
||||
return nil, errs.ErrArgs.WrapMsg("unsupported chain_type: " + rp.ChainType)
|
||||
}
|
||||
}
|
||||
|
||||
// validateCreateHook reserves a centralized validation extension point split by packet type.
|
||||
func (s *redPacketServer) validateCreateHook(ctx context.Context, req *pbredpacket.CreateOrderReq) error {
|
||||
switch req.PacketType {
|
||||
case 0:
|
||||
return s.validateFixedPacketCreate(ctx, req)
|
||||
case 1:
|
||||
return s.validateRandomPacketCreate(ctx, req)
|
||||
case 2:
|
||||
return s.validateTransferPacketCreate(ctx, req)
|
||||
default:
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("unsupported packet_type: %d", req.PacketType))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *redPacketServer) validateFixedPacketCreate(ctx context.Context, req *pbredpacket.CreateOrderReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) validateRandomPacketCreate(ctx context.Context, req *pbredpacket.CreateOrderReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) validateTransferPacketCreate(ctx context.Context, req *pbredpacket.CreateOrderReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildFallbackCreatedPacket(rp *model.RedPacket, packetID string) *createdPacketSnapshot {
|
||||
return &createdPacketSnapshot{
|
||||
PacketID: packetID,
|
||||
ChainID: rp.ChainID,
|
||||
ContractAddress: rp.ContractAddress,
|
||||
CreatorWallet: strings.ToLower(rp.CreatorWallet),
|
||||
PacketType: rp.PacketType,
|
||||
Token: normalizeTokenAddress(rp.Token),
|
||||
TotalAmount: rp.TotalAmount,
|
||||
TotalShares: rp.TotalShares,
|
||||
ExpiryAt: rp.ExpiryAt,
|
||||
}
|
||||
}
|
||||
|
||||
func buildCreatedPacketSnapshot(rp *model.RedPacket, event *chain.ParsedEvent) *createdPacketSnapshot {
|
||||
return &createdPacketSnapshot{
|
||||
PacketID: chain.GetPacketIDFromEvent(event).String(),
|
||||
ChainID: rp.ChainID,
|
||||
ContractAddress: rp.ContractAddress,
|
||||
CreatorWallet: strings.ToLower(chain.GetAddressFromEvent(event, "creator").Hex()),
|
||||
PacketType: int32(chain.GetUintFromEvent(event, "packetType").Int64()),
|
||||
Token: strings.ToLower(chain.GetAddressFromEvent(event, "token").Hex()),
|
||||
TotalAmount: chain.GetUintFromEvent(event, "totalAmount").String(),
|
||||
TotalShares: int32(chain.GetUintFromEvent(event, "totalShares").Int64()),
|
||||
ExpiryAt: chain.GetUintFromEvent(event, "expiryAt").Int64(),
|
||||
}
|
||||
}
|
||||
|
||||
func validateCreatedPacket(rp *model.RedPacket, createdPacket *createdPacketSnapshot) error {
|
||||
if createdPacket == nil {
|
||||
return errs.ErrInternalServer.WrapMsg("created packet is nil")
|
||||
}
|
||||
if createdPacket.CreatorWallet != "" && strings.ToLower(rp.CreatorWallet) != createdPacket.CreatorWallet {
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("creator mismatch: got %s want %s", createdPacket.CreatorWallet, rp.CreatorWallet))
|
||||
}
|
||||
if createdPacket.PacketType != rp.PacketType {
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("packet type mismatch: got %d want %d", createdPacket.PacketType, rp.PacketType))
|
||||
}
|
||||
if createdPacket.TotalAmount != rp.TotalAmount {
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("total amount mismatch: got %s want %s", createdPacket.TotalAmount, rp.TotalAmount))
|
||||
}
|
||||
if createdPacket.TotalShares != rp.TotalShares {
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("total shares mismatch: got %d want %d", createdPacket.TotalShares, rp.TotalShares))
|
||||
}
|
||||
expectedToken := normalizeTokenAddress(rp.Token)
|
||||
if createdPacket.Token != expectedToken {
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("token mismatch: got %s want %s", createdPacket.Token, expectedToken))
|
||||
}
|
||||
if rp.ExpiryAt > 0 && createdPacket.ExpiryAt != rp.ExpiryAt {
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("expiry mismatch: got %d want %d", createdPacket.ExpiryAt, rp.ExpiryAt))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateClaimBase(rp *model.RedPacket, userID, claimer string) error {
|
||||
if rp == nil {
|
||||
return servererrs.ErrRecordNotFound.WrapMsg("packet not found")
|
||||
}
|
||||
if strings.TrimSpace(userID) == "" {
|
||||
return errs.ErrArgs.WrapMsg("user_id is required")
|
||||
}
|
||||
if strings.TrimSpace(claimer) == "" {
|
||||
return errs.ErrArgs.WrapMsg("claimer is required")
|
||||
}
|
||||
if rp.Status != "ACTIVE" {
|
||||
return errs.ErrArgs.WrapMsg("packet is not active, current status: " + rp.Status)
|
||||
}
|
||||
if rp.ExpiryAt > 0 && rp.ExpiryAt <= time.Now().Unix() {
|
||||
return errs.ErrArgs.WrapMsg("packet is expired")
|
||||
}
|
||||
if rp.Status == "REFUNDED" {
|
||||
return errs.ErrArgs.WrapMsg("packet is refunded")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) validateFixedPacketClaim(ctx context.Context, rp *model.RedPacket, userID, claimer string) error {
|
||||
if strings.TrimSpace(rp.GroupID) == "" {
|
||||
return errs.ErrArgs.WrapMsg("group_id is required for fixed packet claim")
|
||||
}
|
||||
if err := s.ensureNotClaimed(ctx, rp.PacketID, userID, claimer); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ensureGroupEligibility(ctx, rp.GroupID, userID)
|
||||
}
|
||||
|
||||
func (s *redPacketServer) validateRandomPacketClaim(ctx context.Context, rp *model.RedPacket, userID, claimer string) error {
|
||||
if strings.TrimSpace(rp.GroupID) == "" {
|
||||
return errs.ErrArgs.WrapMsg("group_id is required for random packet claim")
|
||||
}
|
||||
if err := s.ensureNotClaimed(ctx, rp.PacketID, userID, claimer); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ensureGroupEligibility(ctx, rp.GroupID, userID)
|
||||
}
|
||||
|
||||
func (s *redPacketServer) validateTransferPacketClaim(ctx context.Context, rp *model.RedPacket, userID, claimer string) error {
|
||||
if err := s.ensureNotClaimed(ctx, rp.PacketID, userID, claimer); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(rp.ReceiverUserID) == "" {
|
||||
return errs.ErrArgs.WrapMsg("receiver_user_id is required for transfer claim")
|
||||
}
|
||||
if rp.ReceiverUserID != userID {
|
||||
return errs.ErrNoPermission.WrapMsg("user is not the designated receiver")
|
||||
}
|
||||
return s.ensureFriendRelationship(ctx, rp.CreatorUserID, userID)
|
||||
}
|
||||
|
||||
func (s *redPacketServer) ensureNotClaimed(ctx context.Context, packetID, userID, claimer string) error {
|
||||
if strings.TrimSpace(userID) != "" {
|
||||
claim, err := s.db.GetClaimByPacketIDAndUserID(ctx, packetID, userID)
|
||||
if err == nil && claim != nil && claim.Status != "FAILED" {
|
||||
return errs.ErrArgs.WrapMsg("user already claimed")
|
||||
}
|
||||
if err != nil && !errs.ErrRecordNotFound.Is(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
claim, err := s.db.GetClaimByPacketIDAndClaimer(ctx, packetID, claimer)
|
||||
if err == nil && claim != nil && claim.Status != "FAILED" {
|
||||
return errs.ErrArgs.WrapMsg("already claimed")
|
||||
}
|
||||
if err != nil && !errs.ErrRecordNotFound.Is(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) ensureWalletBinding(ctx context.Context, userID, claimer, chainType string) error {
|
||||
if _, err := s.db.GetActiveWalletBinding(ctx, userID, chainType, claimer); err != nil {
|
||||
if errs.ErrRecordNotFound.Is(err) {
|
||||
return errs.ErrNoPermission.WrapMsg("wallet is not bound to user")
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureGroupEligibility reserves centralized group membership checks.
|
||||
func (s *redPacketServer) ensureGroupEligibility(ctx context.Context, groupID, userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureFriendRelationship reserves centralized relation validation for transfer packets.
|
||||
func (s *redPacketServer) ensureFriendRelationship(ctx context.Context, creatorUserID, receiverUserID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) resolveClaimedEvent(ctx context.Context, rp *model.RedPacket, txHash string) (*claimedEventSnapshot, error) {
|
||||
var (
|
||||
events []*chain.ParsedEvent
|
||||
err error
|
||||
)
|
||||
|
||||
switch rp.ChainType {
|
||||
case "EVM":
|
||||
if s.chainClient == nil {
|
||||
return nil, nil
|
||||
}
|
||||
events, err = s.chainClient.ParseTransactionReceipt(ctx, common.HexToHash(txHash))
|
||||
case "TRON":
|
||||
if s.tronClient == nil {
|
||||
return nil, nil
|
||||
}
|
||||
events, err = s.tronClient.ParseTransactionReceipt(ctx, txHash)
|
||||
default:
|
||||
return nil, errs.ErrArgs.WrapMsg("unsupported chain_type: " + rp.ChainType)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if event.Name != "PacketClaimed" {
|
||||
continue
|
||||
}
|
||||
packetID := chain.GetPacketIDFromEvent(event).String()
|
||||
claimerWallet := strings.ToLower(chain.GetAddressFromEvent(event, "claimer").Hex())
|
||||
if packetID != rp.PacketID {
|
||||
return nil, errs.ErrArgs.WrapMsg(fmt.Sprintf("claim event packet mismatch: got %s want %s", packetID, rp.PacketID))
|
||||
}
|
||||
return &claimedEventSnapshot{
|
||||
ClaimerWallet: claimerWallet,
|
||||
AuthNonce: chain.GetUintFromEvent(event, "authNonce").String(),
|
||||
Amount: chain.GetAmountFromEvent(event).String(),
|
||||
BlockNumber: event.BlockNumber,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func derivePacketStatusAfterClaim(rp *model.RedPacket, claimedAmount string) string {
|
||||
if rp == nil {
|
||||
return ""
|
||||
}
|
||||
if rp.PacketType == 2 {
|
||||
return "COMPLETED"
|
||||
}
|
||||
|
||||
nextShares := rp.ClaimedShares + 1
|
||||
if rp.TotalShares > 0 && nextShares >= rp.TotalShares {
|
||||
return "COMPLETED"
|
||||
}
|
||||
|
||||
totalClaimed := addNumericStrings(rp.ClaimedAmount, claimedAmount)
|
||||
if rp.TotalAmount != "" && totalClaimed == rp.TotalAmount {
|
||||
return "COMPLETED"
|
||||
}
|
||||
|
||||
return "ACTIVE"
|
||||
}
|
||||
|
||||
func addNumericStrings(current, delta string) string {
|
||||
left := new(big.Int)
|
||||
if current != "" {
|
||||
left.SetString(current, 10)
|
||||
}
|
||||
right := new(big.Int)
|
||||
if delta != "" {
|
||||
right.SetString(delta, 10)
|
||||
}
|
||||
return new(big.Int).Add(left, right).String()
|
||||
}
|
||||
|
||||
func normalizeScopeType(scopeType string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(scopeType)) {
|
||||
case "GROUP", "DIRECT", "PUBLIC":
|
||||
return strings.ToUpper(strings.TrimSpace(scopeType))
|
||||
default:
|
||||
return "PUBLIC"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeChainType(chainType string) (string, error) {
|
||||
switch strings.ToUpper(strings.TrimSpace(chainType)) {
|
||||
case "EVM":
|
||||
return "EVM", nil
|
||||
case "TRON":
|
||||
return "TRON", nil
|
||||
default:
|
||||
return "", errs.ErrArgs.WrapMsg("unsupported chain_type: " + chainType)
|
||||
}
|
||||
}
|
||||
|
||||
func validateCreateScope(scopeType, groupID, receiverUserID string, receiverUserIDs []string) error {
|
||||
switch scopeType {
|
||||
case "GROUP":
|
||||
if strings.TrimSpace(groupID) == "" {
|
||||
return errs.ErrArgs.WrapMsg("group_id is required when scope_type=GROUP")
|
||||
}
|
||||
case "DIRECT":
|
||||
if strings.TrimSpace(receiverUserID) == "" && len(receiverUserIDs) == 0 {
|
||||
return errs.ErrArgs.WrapMsg("receiver_user_id or receiver_user_ids is required when scope_type=DIRECT")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeTokenAddress(token string) string {
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return strings.ToLower(common.Address{}.Hex())
|
||||
}
|
||||
return strings.ToLower(common.HexToAddress(token).Hex())
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func redPacketModelToProto(rp *model.RedPacket) *pbredpacket.RedPacketRecord {
|
||||
if rp == nil {
|
||||
return nil
|
||||
}
|
||||
return &pbredpacket.RedPacketRecord{
|
||||
BizID: rp.BizID,
|
||||
ChainType: rp.ChainType,
|
||||
PacketID: rp.PacketID,
|
||||
ChainID: rp.ChainID,
|
||||
ContractAddress: rp.ContractAddress,
|
||||
CreatorUserID: rp.CreatorUserID,
|
||||
CreatorWallet: rp.CreatorWallet,
|
||||
GroupID: rp.GroupID,
|
||||
ScopeType: rp.ScopeType,
|
||||
ReceiverUserID: rp.ReceiverUserID,
|
||||
ReceiverUserIDs: append([]string(nil), rp.ReceiverUserIDs...),
|
||||
PacketType: rp.PacketType,
|
||||
Token: rp.Token,
|
||||
TotalAmount: rp.TotalAmount,
|
||||
TotalShares: rp.TotalShares,
|
||||
ClaimedAmount: rp.ClaimedAmount,
|
||||
ClaimedShares: rp.ClaimedShares,
|
||||
ExpiryAt: rp.ExpiryAt,
|
||||
TxHash: rp.TxHash,
|
||||
Status: rp.Status,
|
||||
CreatedAt: rp.CreatedAt.Unix(),
|
||||
UpdatedAt: rp.UpdatedAt.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func claimsModelToProto(claims []*model.RedPacketClaim) []*pbredpacket.RedPacketClaimRecord {
|
||||
out := make([]*pbredpacket.RedPacketClaimRecord, 0, len(claims))
|
||||
for _, c := range claims {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, &pbredpacket.RedPacketClaimRecord{
|
||||
PacketID: c.PacketID,
|
||||
UserID: c.UserID,
|
||||
ClaimerWallet: c.ClaimerWallet,
|
||||
AuthNonce: c.AuthNonce,
|
||||
ClaimTxHash: c.ClaimTxHash,
|
||||
ClaimedAmount: c.ClaimedAmount,
|
||||
BlockNumber: c.BlockNumber,
|
||||
Status: c.Status,
|
||||
CreatedAt: c.CreatedAt.Unix(),
|
||||
UpdatedAt: c.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@ -0,0 +1,251 @@
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/google/uuid"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
pbredpacket "github.com/openimsdk/protocol/redpacket"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
func (s *redPacketServer) IssueWalletBindChallenge(ctx context.Context, req *pbredpacket.IssueWalletBindChallengeReq) (*pbredpacket.IssueWalletBindChallengeResp, error) {
|
||||
currentUserID := mcontext.GetOpUserID(ctx)
|
||||
if currentUserID == "" {
|
||||
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
|
||||
}
|
||||
|
||||
chainType, err := normalizeChainType(req.ChainType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
walletAddress := strings.TrimSpace(req.WalletAddress)
|
||||
if walletAddress == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("wallet_address is required")
|
||||
}
|
||||
|
||||
challengeID := uuid.NewString()
|
||||
nonce := uuid.NewString()
|
||||
issuedAt := time.Now().UTC()
|
||||
expiresAt := issuedAt.Add(10 * time.Minute)
|
||||
|
||||
protocol := "siwe-eip4361"
|
||||
signMethod := "personal_sign"
|
||||
message := buildEVMBindMessage(currentUserID, walletAddress, req.Domain, req.Uri, req.ChainID, challengeID, nonce, issuedAt, expiresAt)
|
||||
if chainType == "TRON" {
|
||||
protocol = "tron-signmessagev2"
|
||||
signMethod = "signMessageV2"
|
||||
message = buildTRONBindMessage(currentUserID, walletAddress, req.ChainID, challengeID, nonce, issuedAt, expiresAt)
|
||||
}
|
||||
|
||||
challenge := &model.WalletBindingChallenge{
|
||||
ChallengeID: challengeID,
|
||||
UserID: currentUserID,
|
||||
ChainType: chainType,
|
||||
ChainID: req.ChainID,
|
||||
WalletAddress: walletAddress,
|
||||
Nonce: nonce,
|
||||
Message: message,
|
||||
Protocol: protocol,
|
||||
SignMethod: signMethod,
|
||||
Status: "PENDING",
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: issuedAt,
|
||||
UpdatedAt: issuedAt,
|
||||
}
|
||||
if err := s.db.CreateWalletBindingChallenge(ctx, challenge); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pbredpacket.IssueWalletBindChallengeResp{
|
||||
ChallengeID: challengeID,
|
||||
UserID: currentUserID,
|
||||
ChainType: chainType,
|
||||
ChainID: req.ChainID,
|
||||
Wallet: walletAddress,
|
||||
Protocol: protocol,
|
||||
SignMethod: signMethod,
|
||||
Nonce: nonce,
|
||||
Message: message,
|
||||
IssuedAt: issuedAt.Format(time.RFC3339),
|
||||
ExpiresAt: expiresAt.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) ConfirmWalletBind(ctx context.Context, req *pbredpacket.ConfirmWalletBindReq) (*pbredpacket.ConfirmWalletBindResp, error) {
|
||||
if strings.TrimSpace(req.ChallengeID) == "" || strings.TrimSpace(req.Signature) == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("challenge_id and signature are required")
|
||||
}
|
||||
challenge, err := s.db.GetWalletBindingChallenge(ctx, req.ChallengeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if challenge.Status != "PENDING" {
|
||||
return nil, errs.ErrArgs.WrapMsg("challenge is not pending")
|
||||
}
|
||||
if time.Now().UTC().After(challenge.ExpiresAt) {
|
||||
challenge.Status = "EXPIRED"
|
||||
challenge.UpdatedAt = time.Now()
|
||||
_ = s.db.UpdateWalletBindingChallenge(ctx, challenge)
|
||||
return nil, errs.ErrArgs.WrapMsg("challenge is expired")
|
||||
}
|
||||
|
||||
switch challenge.ChainType {
|
||||
case "EVM":
|
||||
if err := verifyEVMBindSignature(challenge.Message, challenge.WalletAddress, req.Signature); err != nil {
|
||||
challenge.Status = "FAILED"
|
||||
challenge.Signature = req.Signature
|
||||
challenge.UpdatedAt = time.Now()
|
||||
_ = s.db.UpdateWalletBindingChallenge(ctx, challenge)
|
||||
return nil, err
|
||||
}
|
||||
case "TRON":
|
||||
return nil, errs.ErrInternalServer.WrapMsg("TRON wallet binding verification is not implemented yet")
|
||||
default:
|
||||
return nil, errs.ErrArgs.WrapMsg("unsupported chain_type: " + challenge.ChainType)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
challenge.Status = "VERIFIED"
|
||||
challenge.Signature = req.Signature
|
||||
challenge.VerifiedAt = &now
|
||||
challenge.UpdatedAt = now
|
||||
if err := s.db.UpdateWalletBindingChallenge(ctx, challenge); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
binding := &model.WalletBinding{
|
||||
UserID: challenge.UserID,
|
||||
ChainType: challenge.ChainType,
|
||||
ChainID: challenge.ChainID,
|
||||
WalletAddress: challenge.WalletAddress,
|
||||
Status: "ACTIVE",
|
||||
ChallengeID: challenge.ChallengeID,
|
||||
VerifiedAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := s.db.UpsertWalletBinding(ctx, binding); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pbredpacket.ConfirmWalletBindResp{
|
||||
UserID: binding.UserID,
|
||||
ChainType: binding.ChainType,
|
||||
ChainID: binding.ChainID,
|
||||
WalletAddress: binding.WalletAddress,
|
||||
Status: binding.Status,
|
||||
VerifiedAt: binding.VerifiedAt.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) GetWalletBinding(ctx context.Context, req *pbredpacket.GetWalletBindingReq) (*pbredpacket.GetWalletBindingResp, error) {
|
||||
currentUserID := mcontext.GetOpUserID(ctx)
|
||||
if currentUserID == "" {
|
||||
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
|
||||
}
|
||||
|
||||
normalizedChainType, err := normalizeChainType(req.ChainType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
binding, err := s.db.GetActiveWalletBinding(ctx, currentUserID, normalizedChainType, req.WalletAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbredpacket.GetWalletBindingResp{
|
||||
UserID: binding.UserID,
|
||||
ChainType: binding.ChainType,
|
||||
ChainID: binding.ChainID,
|
||||
WalletAddress: binding.WalletAddress,
|
||||
Status: binding.Status,
|
||||
ChallengeID: binding.ChallengeID,
|
||||
VerifiedAt: binding.VerifiedAt.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildEVMBindMessage(userID, walletAddress, domainIn, uriIn string, chainID int64, challengeID, nonce string, issuedAt, expiresAt time.Time) string {
|
||||
domain := strings.TrimSpace(domainIn)
|
||||
if domain == "" {
|
||||
domain = "redpacket"
|
||||
}
|
||||
uri := strings.TrimSpace(uriIn)
|
||||
if uri == "" {
|
||||
uri = "https://redpacket.local/wallet-bind"
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s wants you to sign in with your Ethereum account:\n", domain)
|
||||
b.WriteString(strings.TrimSpace(walletAddress))
|
||||
b.WriteString("\n\n")
|
||||
fmt.Fprintf(&b, "Bind wallet %s to user %s.\n", strings.TrimSpace(walletAddress), strings.TrimSpace(userID))
|
||||
fmt.Fprintf(&b, "URI: %s\n", uri)
|
||||
fmt.Fprintf(&b, "Version: 1\n")
|
||||
fmt.Fprintf(&b, "Chain ID: %d\n", chainID)
|
||||
fmt.Fprintf(&b, "Nonce: %s\n", nonce)
|
||||
fmt.Fprintf(&b, "Issued At: %s\n", issuedAt.Format(time.RFC3339))
|
||||
fmt.Fprintf(&b, "Expiration Time: %s\n", expiresAt.Format(time.RFC3339))
|
||||
fmt.Fprintf(&b, "Request ID: %s", challengeID)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func buildTRONBindMessage(userID, walletAddress string, chainID int64, challengeID, nonce string, issuedAt, expiresAt time.Time) string {
|
||||
return fmt.Sprintf(
|
||||
"Bind TRON wallet %s to user %s\nchallenge_id: %s\nnonce: %s\nchain_id: %d\nissued_at: %s\nexpires_at: %s",
|
||||
strings.TrimSpace(walletAddress),
|
||||
strings.TrimSpace(userID),
|
||||
challengeID,
|
||||
nonce,
|
||||
chainID,
|
||||
issuedAt.Format(time.RFC3339),
|
||||
expiresAt.Format(time.RFC3339),
|
||||
)
|
||||
}
|
||||
|
||||
func verifyEVMBindSignature(message, walletAddress, signature string) error {
|
||||
if strings.TrimSpace(message) == "" {
|
||||
return errs.ErrArgs.WrapMsg("bind message is empty")
|
||||
}
|
||||
if !common.IsHexAddress(walletAddress) {
|
||||
return errs.ErrArgs.WrapMsg("invalid evm wallet address")
|
||||
}
|
||||
|
||||
sig, err := hex.DecodeString(strings.TrimPrefix(signature, "0x"))
|
||||
if err != nil {
|
||||
return errs.ErrArgs.WrapMsg("decode signature failed: " + err.Error())
|
||||
}
|
||||
if len(sig) != 65 {
|
||||
return errs.ErrArgs.WrapMsg(fmt.Sprintf("invalid signature length: %d", len(sig)))
|
||||
}
|
||||
if sig[64] >= 27 {
|
||||
sig[64] -= 27
|
||||
}
|
||||
if sig[64] > 1 {
|
||||
return errs.ErrArgs.WrapMsg("invalid signature recovery id")
|
||||
}
|
||||
|
||||
hash := crypto.Keccak256Hash([]byte(personalSignMessage(message)))
|
||||
pubKey, err := crypto.SigToPub(hash.Bytes(), sig)
|
||||
if err != nil {
|
||||
return errs.ErrInternalServer.WrapMsg("recover signer failed: " + err.Error())
|
||||
}
|
||||
|
||||
recovered := crypto.PubkeyToAddress(*pubKey)
|
||||
if !strings.EqualFold(recovered.Hex(), walletAddress) {
|
||||
return errs.ErrNoPermission.WrapMsg("signature does not match wallet address")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func personalSignMessage(message string) string {
|
||||
return fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message)
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/internal/rpc/redpacket"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc"
|
||||
"github.com/openimsdk/open-im-server/v3/version"
|
||||
"github.com/openimsdk/tools/system/program"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type RedPacketRpcCmd struct {
|
||||
*RootCmd
|
||||
ctx context.Context
|
||||
configMap map[string]any
|
||||
redPacketConfig *redpacket.Config
|
||||
}
|
||||
|
||||
func NewRedPacketRpcCmd() *RedPacketRpcCmd {
|
||||
var redPacketConfig redpacket.Config
|
||||
ret := &RedPacketRpcCmd{redPacketConfig: &redPacketConfig}
|
||||
ret.configMap = map[string]any{
|
||||
OpenIMRPCRedPacketCfgFileName: &redPacketConfig.RpcConfig,
|
||||
MongodbConfigFileName: &redPacketConfig.MongodbConfig,
|
||||
ShareFileName: &redPacketConfig.Share,
|
||||
DiscoveryConfigFilename: &redPacketConfig.Discovery,
|
||||
}
|
||||
ret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))
|
||||
ret.ctx = context.WithValue(context.Background(), "version", version.Version)
|
||||
ret.Command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return ret.runE()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *RedPacketRpcCmd) Exec() error {
|
||||
return c.Execute()
|
||||
}
|
||||
|
||||
func (c *RedPacketRpcCmd) runE() error {
|
||||
return startrpc.Start(c.ctx, &c.redPacketConfig.Discovery, &c.redPacketConfig.RpcConfig.Prometheus, c.redPacketConfig.RpcConfig.RPC.ListenIP,
|
||||
c.redPacketConfig.RpcConfig.RPC.RegisterIP, c.redPacketConfig.RpcConfig.RPC.AutoSetPorts, c.redPacketConfig.RpcConfig.RPC.Ports,
|
||||
c.Index(), c.redPacketConfig.Share.RpcRegisterName.RedPacket, &c.redPacketConfig.Share, c.redPacketConfig,
|
||||
nil,
|
||||
redpacket.Start)
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
)
|
||||
|
||||
// RedPacketDatabase is a façade aggregating all redpacket-related collections.
|
||||
// It mirrors the legacy Repository interface so the rpc service layer stays
|
||||
// unaware of the underlying storage.
|
||||
type RedPacketDatabase interface {
|
||||
CreateRedPacket(ctx context.Context, rp *model.RedPacket) error
|
||||
GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error)
|
||||
GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error)
|
||||
UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error
|
||||
UpdateRedPacketStatus(ctx context.Context, packetID, status string) error
|
||||
UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error
|
||||
|
||||
CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error
|
||||
GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error)
|
||||
MarkClaimAuthUsed(ctx context.Context, authNonce string) error
|
||||
|
||||
SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error
|
||||
GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error)
|
||||
GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error)
|
||||
GetClaimsByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error)
|
||||
|
||||
SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error
|
||||
|
||||
CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
|
||||
GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error)
|
||||
UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
|
||||
|
||||
UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error
|
||||
GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error)
|
||||
}
|
||||
|
||||
type redPacketDatabase struct {
|
||||
rp database.RedPacket
|
||||
claim database.RedPacketClaim
|
||||
claimAuth database.RedPacketClaimAuth
|
||||
refund database.RedPacketRefund
|
||||
challenge database.WalletBindingChallenge
|
||||
binding database.WalletBinding
|
||||
}
|
||||
|
||||
func NewRedPacketDatabase(
|
||||
rp database.RedPacket,
|
||||
claim database.RedPacketClaim,
|
||||
claimAuth database.RedPacketClaimAuth,
|
||||
refund database.RedPacketRefund,
|
||||
challenge database.WalletBindingChallenge,
|
||||
binding database.WalletBinding,
|
||||
) RedPacketDatabase {
|
||||
return &redPacketDatabase{
|
||||
rp: rp,
|
||||
claim: claim,
|
||||
claimAuth: claimAuth,
|
||||
refund: refund,
|
||||
challenge: challenge,
|
||||
binding: binding,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) CreateRedPacket(ctx context.Context, rp *model.RedPacket) error {
|
||||
return d.rp.Create(ctx, rp)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error) {
|
||||
return d.rp.GetByBizID(ctx, bizID)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error) {
|
||||
return d.rp.GetByPacketID(ctx, packetID)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error {
|
||||
return d.rp.UpdateCreated(ctx, rp)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) UpdateRedPacketStatus(ctx context.Context, packetID, status string) error {
|
||||
return d.rp.UpdateStatus(ctx, packetID, status)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error {
|
||||
return d.rp.UpdateClaimProgress(ctx, packetID, claimedAmount, status)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error {
|
||||
return d.claimAuth.Create(ctx, auth)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error) {
|
||||
return d.claimAuth.Get(ctx, packetID, claimer)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) MarkClaimAuthUsed(ctx context.Context, authNonce string) error {
|
||||
return d.claimAuth.MarkUsed(ctx, authNonce)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error {
|
||||
return d.claim.Save(ctx, claim)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error) {
|
||||
return d.claim.GetByPacketIDAndClaimer(ctx, packetID, claimer)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error) {
|
||||
return d.claim.GetByPacketIDAndUserID(ctx, packetID, userID)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) GetClaimsByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error) {
|
||||
return d.claim.ListByPacketID(ctx, packetID)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error {
|
||||
return d.refund.Save(ctx, refund)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
|
||||
return d.challenge.Create(ctx, challenge)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error) {
|
||||
return d.challenge.Get(ctx, challengeID)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
|
||||
return d.challenge.Update(ctx, challenge)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error {
|
||||
return d.binding.Upsert(ctx, binding)
|
||||
}
|
||||
|
||||
func (d *redPacketDatabase) GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error) {
|
||||
return d.binding.GetActive(ctx, userID, chainType, walletAddress)
|
||||
}
|
||||
@ -0,0 +1,456 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// ---- RedPacket ----
|
||||
|
||||
type RedPacketMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func NewRedPacketMongo(db *mongo.Database) (database.RedPacket, error) {
|
||||
coll := db.Collection("red_packet")
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "biz_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "packet_id", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "group_id", Value: 1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RedPacketMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketMgo) Create(ctx context.Context, rp *model.RedPacket) error {
|
||||
_, err := m.coll.InsertOne(ctx, rp)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *RedPacketMgo) GetByBizID(ctx context.Context, bizID string) (*model.RedPacket, error) {
|
||||
var rp model.RedPacket
|
||||
err := m.coll.FindOne(ctx, bson.M{"biz_id": bizID}).Decode(&rp)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("red packet not found", "bizID", bizID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &rp, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketMgo) GetByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error) {
|
||||
var rp model.RedPacket
|
||||
err := m.coll.FindOne(ctx, bson.M{"packet_id": packetID}).Decode(&rp)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &rp, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketMgo) UpdateCreated(ctx context.Context, rp *model.RedPacket) error {
|
||||
updates := bson.M{
|
||||
"chain_type": rp.ChainType,
|
||||
"packet_id": rp.PacketID,
|
||||
"tx_hash": rp.TxHash,
|
||||
"chain_id": rp.ChainID,
|
||||
"contract_address": rp.ContractAddress,
|
||||
"group_id": rp.GroupID,
|
||||
"scope_type": rp.ScopeType,
|
||||
"receiver_user_id": rp.ReceiverUserID,
|
||||
"receiver_user_ids": rp.ReceiverUserIDs,
|
||||
"status": rp.Status,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
res, err := m.coll.UpdateOne(ctx, bson.M{"biz_id": rp.BizID}, bson.M{"$set": updates})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.MatchedCount == 0 {
|
||||
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "bizID", rp.BizID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RedPacketMgo) UpdateStatus(ctx context.Context, packetID, status string) error {
|
||||
res, err := m.coll.UpdateOne(ctx, bson.M{"packet_id": packetID},
|
||||
bson.M{"$set": bson.M{"status": status, "updated_at": time.Now()}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.MatchedCount == 0 {
|
||||
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RedPacketMgo) UpdateClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error {
|
||||
var rp model.RedPacket
|
||||
err := m.coll.FindOne(ctx, bson.M{"packet_id": packetID}).Decode(&rp)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
totalClaimed := addNumericStrings(rp.ClaimedAmount, claimedAmount)
|
||||
nextShares := rp.ClaimedShares + 1
|
||||
updates := bson.M{
|
||||
"claimed_amount": totalClaimed,
|
||||
"claimed_shares": nextShares,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if status != "" {
|
||||
updates["status"] = status
|
||||
}
|
||||
_, err = m.coll.UpdateOne(ctx, bson.M{"packet_id": packetID}, bson.M{"$set": updates})
|
||||
return err
|
||||
}
|
||||
|
||||
func addNumericStrings(current, delta string) string {
|
||||
left := new(big.Int)
|
||||
if current != "" {
|
||||
left.SetString(current, 10)
|
||||
}
|
||||
right := new(big.Int)
|
||||
if delta != "" {
|
||||
right.SetString(delta, 10)
|
||||
}
|
||||
return new(big.Int).Add(left, right).String()
|
||||
}
|
||||
|
||||
// ---- RedPacketClaim ----
|
||||
|
||||
type RedPacketClaimMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func NewRedPacketClaimMongo(db *mongo.Database) (database.RedPacketClaim, error) {
|
||||
coll := db.Collection("red_packet_claim")
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "claim_tx_hash", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "user_id", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "claimer_wallet", Value: 1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RedPacketClaimMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketClaimMgo) Save(ctx context.Context, claim *model.RedPacketClaim) error {
|
||||
if claim.UserID != "" {
|
||||
var existing model.RedPacketClaim
|
||||
err := m.coll.FindOne(ctx, bson.M{
|
||||
"packet_id": claim.PacketID,
|
||||
"user_id": claim.UserID,
|
||||
}).Decode(&existing)
|
||||
if err == nil {
|
||||
updates := bson.M{
|
||||
"claimer_wallet": claim.ClaimerWallet,
|
||||
"auth_nonce": claim.AuthNonce,
|
||||
"claim_tx_hash": claim.ClaimTxHash,
|
||||
"claimed_amount": claim.ClaimedAmount,
|
||||
"block_number": claim.BlockNumber,
|
||||
"status": claim.Status,
|
||||
"updated_at": claim.UpdatedAt,
|
||||
}
|
||||
_, err := m.coll.UpdateOne(ctx,
|
||||
bson.M{"packet_id": claim.PacketID, "user_id": claim.UserID},
|
||||
bson.M{"$set": updates})
|
||||
return err
|
||||
}
|
||||
if err != mongo.ErrNoDocuments {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := m.coll.UpdateOne(ctx,
|
||||
bson.M{"claim_tx_hash": claim.ClaimTxHash},
|
||||
bson.M{"$set": claim},
|
||||
options.Update().SetUpsert(true),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *RedPacketClaimMgo) GetByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error) {
|
||||
var claim model.RedPacketClaim
|
||||
err := m.coll.FindOne(ctx,
|
||||
bson.M{"packet_id": packetID, "claimer_wallet": claimer},
|
||||
options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}}),
|
||||
).Decode(&claim)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("claim not found", "packetID", packetID, "claimer", claimer)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &claim, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketClaimMgo) GetByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error) {
|
||||
var claim model.RedPacketClaim
|
||||
err := m.coll.FindOne(ctx,
|
||||
bson.M{"packet_id": packetID, "user_id": userID},
|
||||
options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}}),
|
||||
).Decode(&claim)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("claim not found", "packetID", packetID, "userID", userID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &claim, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketClaimMgo) ListByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error) {
|
||||
cursor, err := m.coll.Find(ctx,
|
||||
bson.M{"packet_id": packetID},
|
||||
options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var claims []*model.RedPacketClaim
|
||||
if err := cursor.All(ctx, &claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// ---- RedPacketClaimAuth ----
|
||||
|
||||
type RedPacketClaimAuthMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func NewRedPacketClaimAuthMongo(db *mongo.Database) (database.RedPacketClaimAuth, error) {
|
||||
coll := db.Collection("red_packet_claim_auth")
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "auth_nonce", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "claimer", Value: 1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RedPacketClaimAuthMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketClaimAuthMgo) Create(ctx context.Context, auth *model.RedPacketClaimAuth) error {
|
||||
_, err := m.coll.InsertOne(ctx, auth)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *RedPacketClaimAuthMgo) Get(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error) {
|
||||
var auth model.RedPacketClaimAuth
|
||||
err := m.coll.FindOne(ctx, bson.M{
|
||||
"packet_id": packetID,
|
||||
"claimer": claimer,
|
||||
"used": false,
|
||||
}).Decode(&auth)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("claim auth not found", "packetID", packetID, "claimer", claimer)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &auth, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketClaimAuthMgo) MarkUsed(ctx context.Context, authNonce string) error {
|
||||
res, err := m.coll.UpdateOne(ctx,
|
||||
bson.M{"auth_nonce": authNonce},
|
||||
bson.M{"$set": bson.M{"used": true}},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.MatchedCount == 0 {
|
||||
return errs.ErrRecordNotFound.WrapMsg("claim auth not found", "authNonce", authNonce)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- RedPacketRefund ----
|
||||
|
||||
type RedPacketRefundMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func NewRedPacketRefundMongo(db *mongo.Database) (database.RedPacketRefund, error) {
|
||||
coll := db.Collection("red_packet_refund")
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "tx_hash", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RedPacketRefundMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
func (m *RedPacketRefundMgo) Save(ctx context.Context, refund *model.RedPacketRefund) error {
|
||||
_, err := m.coll.UpdateOne(ctx,
|
||||
bson.M{"tx_hash": refund.TxHash},
|
||||
bson.M{"$setOnInsert": refund},
|
||||
options.Update().SetUpsert(true),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---- WalletBindingChallenge ----
|
||||
|
||||
type WalletBindingChallengeMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func NewWalletBindingChallengeMongo(db *mongo.Database) (database.WalletBindingChallenge, error) {
|
||||
coll := db.Collection("wallet_binding_challenge")
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "challenge_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "user_id", Value: 1}},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "wallet_address", Value: 1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WalletBindingChallengeMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
func (m *WalletBindingChallengeMgo) Create(ctx context.Context, challenge *model.WalletBindingChallenge) error {
|
||||
_, err := m.coll.InsertOne(ctx, challenge)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *WalletBindingChallengeMgo) Get(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error) {
|
||||
var c model.WalletBindingChallenge
|
||||
err := m.coll.FindOne(ctx, bson.M{"challenge_id": challengeID}).Decode(&c)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("wallet binding challenge not found", "challengeID", challengeID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (m *WalletBindingChallengeMgo) Update(ctx context.Context, c *model.WalletBindingChallenge) error {
|
||||
updates := bson.M{
|
||||
"status": c.Status,
|
||||
"signature": c.Signature,
|
||||
"verified_at": c.VerifiedAt,
|
||||
"updated_at": c.UpdatedAt,
|
||||
}
|
||||
res, err := m.coll.UpdateOne(ctx, bson.M{"challenge_id": c.ChallengeID}, bson.M{"$set": updates})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.MatchedCount == 0 {
|
||||
return errs.ErrRecordNotFound.WrapMsg("wallet binding challenge not found", "challengeID", c.ChallengeID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- WalletBinding ----
|
||||
|
||||
type WalletBindingMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func NewWalletBindingMongo(db *mongo.Database) (database.WalletBinding, error) {
|
||||
coll := db.Collection("wallet_binding")
|
||||
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "chain_type", Value: 1}, {Key: "wallet_address", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "user_id", Value: 1}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WalletBindingMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
func (m *WalletBindingMgo) Upsert(ctx context.Context, b *model.WalletBinding) error {
|
||||
filter := bson.M{
|
||||
"user_id": b.UserID,
|
||||
"chain_type": b.ChainType,
|
||||
"wallet_address": b.WalletAddress,
|
||||
}
|
||||
updates := bson.M{
|
||||
"chain_id": b.ChainID,
|
||||
"status": b.Status,
|
||||
"challenge_id": b.ChallengeID,
|
||||
"verified_at": b.VerifiedAt,
|
||||
"revoked_at": b.RevokedAt,
|
||||
"updated_at": b.UpdatedAt,
|
||||
}
|
||||
setOnInsert := bson.M{
|
||||
"created_at": b.CreatedAt,
|
||||
}
|
||||
_, err := m.coll.UpdateOne(ctx, filter,
|
||||
bson.M{"$set": updates, "$setOnInsert": setOnInsert},
|
||||
options.Update().SetUpsert(true),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *WalletBindingMgo) GetActive(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error) {
|
||||
var b model.WalletBinding
|
||||
err := m.coll.FindOne(ctx, bson.M{
|
||||
"user_id": userID,
|
||||
"chain_type": chainType,
|
||||
"wallet_address": walletAddress,
|
||||
"status": "ACTIVE",
|
||||
}).Decode(&b)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("active wallet binding not found", "userID", userID, "chainType", chainType, "walletAddress", walletAddress)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
)
|
||||
|
||||
type RedPacket interface {
|
||||
Create(ctx context.Context, rp *model.RedPacket) error
|
||||
GetByBizID(ctx context.Context, bizID string) (*model.RedPacket, error)
|
||||
GetByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error)
|
||||
UpdateCreated(ctx context.Context, rp *model.RedPacket) error
|
||||
UpdateStatus(ctx context.Context, packetID, status string) error
|
||||
UpdateClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error
|
||||
}
|
||||
|
||||
type RedPacketClaim interface {
|
||||
Save(ctx context.Context, claim *model.RedPacketClaim) error
|
||||
GetByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error)
|
||||
GetByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error)
|
||||
ListByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error)
|
||||
}
|
||||
|
||||
type RedPacketClaimAuth interface {
|
||||
Create(ctx context.Context, auth *model.RedPacketClaimAuth) error
|
||||
Get(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error)
|
||||
MarkUsed(ctx context.Context, authNonce string) error
|
||||
}
|
||||
|
||||
type RedPacketRefund interface {
|
||||
Save(ctx context.Context, refund *model.RedPacketRefund) error
|
||||
}
|
||||
|
||||
type WalletBindingChallenge interface {
|
||||
Create(ctx context.Context, challenge *model.WalletBindingChallenge) error
|
||||
Get(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error)
|
||||
Update(ctx context.Context, challenge *model.WalletBindingChallenge) error
|
||||
}
|
||||
|
||||
type WalletBinding interface {
|
||||
Upsert(ctx context.Context, binding *model.WalletBinding) error
|
||||
GetActive(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error)
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type RedPacket struct {
|
||||
BizID string `bson:"biz_id"`
|
||||
ChainType string `bson:"chain_type"`
|
||||
PacketID string `bson:"packet_id"`
|
||||
ChainID int64 `bson:"chain_id"`
|
||||
ContractAddress string `bson:"contract_address"`
|
||||
CreatorUserID string `bson:"creator_user_id"`
|
||||
CreatorWallet string `bson:"creator_wallet"`
|
||||
GroupID string `bson:"group_id"`
|
||||
ScopeType string `bson:"scope_type"`
|
||||
ReceiverUserID string `bson:"receiver_user_id"`
|
||||
ReceiverUserIDs []string `bson:"receiver_user_ids"`
|
||||
PacketType int32 `bson:"packet_type"`
|
||||
Token string `bson:"token"`
|
||||
TotalAmount string `bson:"total_amount"`
|
||||
TotalShares int32 `bson:"total_shares"`
|
||||
ClaimedAmount string `bson:"claimed_amount"`
|
||||
ClaimedShares int32 `bson:"claimed_shares"`
|
||||
ExpiryAt int64 `bson:"expiry_at"`
|
||||
TxHash string `bson:"tx_hash"`
|
||||
Status string `bson:"status"`
|
||||
CreatedAt time.Time `bson:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at"`
|
||||
}
|
||||
|
||||
type RedPacketClaim struct {
|
||||
PacketID string `bson:"packet_id"`
|
||||
UserID string `bson:"user_id"`
|
||||
ClaimerWallet string `bson:"claimer_wallet"`
|
||||
AuthNonce string `bson:"auth_nonce"`
|
||||
ClaimTxHash string `bson:"claim_tx_hash"`
|
||||
ClaimedAmount string `bson:"claimed_amount"`
|
||||
BlockNumber uint64 `bson:"block_number"`
|
||||
Status string `bson:"status"`
|
||||
CreatedAt time.Time `bson:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at"`
|
||||
}
|
||||
|
||||
type RedPacketClaimAuth struct {
|
||||
PacketID string `bson:"packet_id"`
|
||||
Claimer string `bson:"claimer"`
|
||||
AuthNonce string `bson:"auth_nonce"`
|
||||
RandomSeed string `bson:"random_seed"`
|
||||
Deadline int64 `bson:"deadline"`
|
||||
Signature string `bson:"signature"`
|
||||
Used bool `bson:"used"`
|
||||
CreatedAt time.Time `bson:"created_at"`
|
||||
}
|
||||
|
||||
type RedPacketRefund struct {
|
||||
PacketID string `bson:"packet_id"`
|
||||
RefundTo string `bson:"refund_to"`
|
||||
TxHash string `bson:"tx_hash"`
|
||||
Amount string `bson:"amount"`
|
||||
CreatedAt time.Time `bson:"created_at"`
|
||||
}
|
||||
|
||||
type WalletBindingChallenge struct {
|
||||
ChallengeID string `bson:"challenge_id"`
|
||||
UserID string `bson:"user_id"`
|
||||
ChainType string `bson:"chain_type"`
|
||||
ChainID int64 `bson:"chain_id"`
|
||||
WalletAddress string `bson:"wallet_address"`
|
||||
Nonce string `bson:"nonce"`
|
||||
Message string `bson:"message"`
|
||||
Protocol string `bson:"protocol"`
|
||||
SignMethod string `bson:"sign_method"`
|
||||
Status string `bson:"status"`
|
||||
Signature string `bson:"signature"`
|
||||
ExpiresAt time.Time `bson:"expires_at"`
|
||||
VerifiedAt *time.Time `bson:"verified_at,omitempty"`
|
||||
CreatedAt time.Time `bson:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at"`
|
||||
}
|
||||
|
||||
type WalletBinding struct {
|
||||
UserID string `bson:"user_id"`
|
||||
ChainType string `bson:"chain_type"`
|
||||
ChainID int64 `bson:"chain_id"`
|
||||
WalletAddress string `bson:"wallet_address"`
|
||||
Status string `bson:"status"`
|
||||
ChallengeID string `bson:"challenge_id"`
|
||||
VerifiedAt time.Time `bson:"verified_at"`
|
||||
RevokedAt *time.Time `bson:"revoked_at,omitempty"`
|
||||
CreatedAt time.Time `bson:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at"`
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package rpcli
|
||||
|
||||
import (
|
||||
pbredpacket "github.com/openimsdk/protocol/redpacket"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func NewRedPacketClient(cc grpc.ClientConnInterface) *RedPacketClient {
|
||||
return &RedPacketClient{pbredpacket.NewRedPacketClient(cc)}
|
||||
}
|
||||
|
||||
type RedPacketClient struct {
|
||||
pbredpacket.RedPacketClient
|
||||
}
|
||||
@ -1 +1 @@
|
||||
Subproject commit 90aae1d576466a1fa55eba386d1f7a38ca6062d0
|
||||
Subproject commit 9f69daaff1f7b46b971bb7b97cd993cd6302b41e
|
||||
Loading…
Reference in new issue