parent
a2272cab06
commit
9661adcb65
@ -0,0 +1,98 @@
|
||||
# RedPacket Backend Service
|
||||
|
||||
A Web3 Red Packet service supporting Ethereum and TRON, following the design documents:
|
||||
|
||||
- `backend-api.md` - API specifications
|
||||
- `redpacket-web3-integration-design.md` - Architecture and flows
|
||||
- `red-packet-go-backend-eth-tron.md` - Blockchain integration details
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Create red packet orders (`/api/redpacket/create-order`)
|
||||
- ✅ Created callback for on-chain transaction results
|
||||
- ✅ Red packet detail query with claim history
|
||||
- ✅ Claim signature issuance (`/api/redpacket/claim-sign`)
|
||||
- ✅ Claim result reporting
|
||||
- ✅ SQLite/MySQL support
|
||||
- ✅ Blockchain signature logic ready for ETH/TRON
|
||||
- ✅ Admin configuration endpoints
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd cmd/openim-rpc/openim-rpc-redpacket
|
||||
|
||||
# 1. Configure (optional)
|
||||
cp config/config.yaml config/config.yaml.bak
|
||||
# Edit config/config.yaml with your blockchain settings
|
||||
|
||||
# 2. Build and run
|
||||
go run .
|
||||
|
||||
# Or build binary
|
||||
go build -o redpacket .
|
||||
./redpacket
|
||||
```
|
||||
|
||||
Service will start on `http://localhost:8080`
|
||||
|
||||
## Test the API
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# Create red packet
|
||||
curl -X POST http://localhost:8080/api/redpacket/create-order \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"creator_user_id": "u1001",
|
||||
"creator_wallet": "0x1111111111111111111111111111111111111111",
|
||||
"packet_type": 1,
|
||||
"total_amount": "1000000000000000000",
|
||||
"total_shares": 10
|
||||
}'
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── config/ # Configuration
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP handlers (Gin)
|
||||
│ ├── model/ # Database models (GORM)
|
||||
│ ├── repository/ # Data access layer
|
||||
│ ├── service/ # Business logic
|
||||
│ └── chain/ # Blockchain integration (to be expanded)
|
||||
├── pkg/resp/ # Response helpers
|
||||
├── router/ # Route definitions
|
||||
├── main.go
|
||||
├── go.mod
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Next Steps (from design docs)
|
||||
|
||||
1. **Full Blockchain Integration**
|
||||
- Implement `ChainClient` for ETH and TRON
|
||||
- Add event indexer for `PacketCreated`, `PacketClaimed`, `PacketRefunded`
|
||||
- Implement proper signature generation using `getSignMessage`
|
||||
|
||||
2. **Advanced Features**
|
||||
- Admin configuration APIs (`setSigner`, `setToken`, etc.)
|
||||
- Refund logic
|
||||
- Rate limiting and authentication
|
||||
- Monitoring and metrics
|
||||
|
||||
3. **Production**
|
||||
- Add proper authentication middleware
|
||||
- Configure production database
|
||||
- Set up monitoring and logging
|
||||
- Deploy with Docker/K8s
|
||||
|
||||
See the three design documents for detailed specifications.
|
||||
|
||||
## API Documentation
|
||||
|
||||
See `backend-api.md` for complete API reference with examples.
|
||||
@ -0,0 +1,71 @@
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
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: 100000000
|
||||
|
||||
indexer:
|
||||
poll_interval: 5
|
||||
@ -0,0 +1,68 @@
|
||||
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
|
||||
)
|
||||
@ -0,0 +1,260 @@
|
||||
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=
|
||||
@ -0,0 +1,65 @@
|
||||
[
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "packetId", "type": "uint256" },
|
||||
{ "indexed": true, "name": "creator", "type": "address" },
|
||||
{ "indexed": false, "name": "packetType", "type": "uint8" },
|
||||
{ "indexed": false, "name": "token", "type": "address" },
|
||||
{ "indexed": false, "name": "totalAmount", "type": "uint256" },
|
||||
{ "indexed": false, "name": "totalShares", "type": "uint256" },
|
||||
{ "indexed": false, "name": "expiryAt", "type": "uint256" }
|
||||
],
|
||||
"name": "PacketCreated",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "packetId", "type": "uint256" },
|
||||
{ "indexed": true, "name": "claimer", "type": "address" },
|
||||
{ "indexed": false, "name": "amount", "type": "uint256" },
|
||||
{ "indexed": false, "name": "authNonce", "type": "uint256" },
|
||||
{ "indexed": false, "name": "randomSeed", "type": "uint256" },
|
||||
{ "indexed": false, "name": "blockNumber", "type": "uint256" }
|
||||
],
|
||||
"name": "PacketClaimed",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "packetId", "type": "uint256" },
|
||||
{ "indexed": true, "name": "refundTo", "type": "address" },
|
||||
{ "indexed": false, "name": "amount", "type": "uint256" }
|
||||
],
|
||||
"name": "PacketRefunded",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{ "name": "packetId", "type": "uint256" },
|
||||
{ "name": "claimer", "type": "address" },
|
||||
{ "name": "authNonce", "type": "uint256" },
|
||||
{ "name": "randomSeed", "type": "uint256" },
|
||||
{ "name": "deadline", "type": "uint256" }
|
||||
],
|
||||
"name": "getSignMessage",
|
||||
"outputs": [{ "name": "", "type": "bytes32" }],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{ "name": "packetId", "type": "uint256" },
|
||||
{ "name": "authNonce", "type": "uint256" },
|
||||
{ "name": "randomSeed", "type": "uint256" },
|
||||
{ "name": "deadline", "type": "uint256" },
|
||||
{ "name": "signature", "type": "bytes" }
|
||||
],
|
||||
"name": "claim",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,143 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
// ChainClient handles blockchain interactions for RedPacket
|
||||
type ChainClient struct {
|
||||
client *ethclient.Client
|
||||
contractABI abi.ABI
|
||||
contractAddr common.Address
|
||||
signerKey *ecdsa.PrivateKey
|
||||
configAdminKey *ecdsa.PrivateKey
|
||||
chainID *big.Int
|
||||
}
|
||||
|
||||
// NewClient creates a new ChainClient
|
||||
func NewClient(rpcURL, contractAddress string, chainID int64, signerPrivateKey, configAdminPrivateKey string) (*ChainClient, error) {
|
||||
client, err := ethclient.Dial(rpcURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to ethereum: %w", err)
|
||||
}
|
||||
|
||||
// Load ABI
|
||||
abiJSON, err := ExtractABIFromEmbeddedArtifact()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load ABI: %w", err)
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(string(abiJSON)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ABI: %w", err)
|
||||
}
|
||||
|
||||
contractAddr := common.HexToAddress(contractAddress)
|
||||
|
||||
var signerKey *ecdsa.PrivateKey
|
||||
if signerPrivateKey != "" {
|
||||
signerKey, err = crypto.HexToECDSA(strings.TrimPrefix(signerPrivateKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid signer private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var adminKey *ecdsa.PrivateKey
|
||||
if configAdminPrivateKey != "" {
|
||||
adminKey, err = crypto.HexToECDSA(strings.TrimPrefix(configAdminPrivateKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid config admin private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &ChainClient{
|
||||
client: client,
|
||||
contractABI: parsedABI,
|
||||
contractAddr: contractAddr,
|
||||
signerKey: signerKey,
|
||||
configAdminKey: adminKey,
|
||||
chainID: big.NewInt(chainID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSignMessage calls contract's getSignMessage view function
|
||||
func (c *ChainClient) GetSignMessage(ctx context.Context, packetID *big.Int, claimer common.Address, authNonce, randomSeed, deadline *big.Int) ([32]byte, error) {
|
||||
var digest [32]byte
|
||||
|
||||
data, err := c.contractABI.Pack("getSignMessage", packetID, claimer, authNonce, randomSeed, deadline)
|
||||
if err != nil {
|
||||
return digest, fmt.Errorf("failed to pack getSignMessage: %w", err)
|
||||
}
|
||||
|
||||
msg := ethereum.CallMsg{
|
||||
To: &c.contractAddr,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := c.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return digest, fmt.Errorf("call getSignMessage failed: %w", err)
|
||||
}
|
||||
|
||||
copy(digest[:], result)
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
// SignClaim signs the digest using the signer key (naked signature as per contract)
|
||||
func (c *ChainClient) SignClaim(digest [32]byte) ([]byte, error) {
|
||||
if c.signerKey == nil {
|
||||
return nil, fmt.Errorf("signer key not configured")
|
||||
}
|
||||
|
||||
sig, err := crypto.Sign(digest[:], c.signerKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign failed: %w", err)
|
||||
}
|
||||
|
||||
// Adjust v from 0/1 to 27/28 as expected by EVM
|
||||
if len(sig) == 65 && sig[64] < 27 {
|
||||
sig[64] += 27
|
||||
}
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// ParseTransactionReceipt parses events from a transaction receipt
|
||||
func (c *ChainClient) ParseTransactionReceipt(ctx context.Context, txHash common.Hash) ([]*ParsedEvent, error) {
|
||||
receipt, err := c.client.TransactionReceipt(ctx, txHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get receipt failed: %w", err)
|
||||
}
|
||||
|
||||
return ParseEventsFromLogs(receipt.Logs, c.contractABI)
|
||||
}
|
||||
|
||||
// Close closes the client connection
|
||||
func (c *ChainClient) Close() {
|
||||
if c.client != nil {
|
||||
c.client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractABIFromEmbeddedArtifact returns the embedded contract ABI
|
||||
func ExtractABIFromEmbeddedArtifact() ([]byte, error) {
|
||||
// In production, this would be embedded with go:embed
|
||||
// For now, we return a simple version. In real implementation, use:
|
||||
// var abiJSON embed.FS
|
||||
// data, _ := abiJSON.ReadFile("abi/RedPacket.json")
|
||||
return []byte(`[
|
||||
{"anonymous":false,"inputs":[{"indexed":true,"name":"packetId","type":"uint256"},{"indexed":true,"name":"creator","type":"address"},{"indexed":false,"name":"packetType","type":"uint8"},{"indexed":false,"name":"token","type":"address"},{"indexed":false,"name":"totalAmount","type":"uint256"},{"indexed":false,"name":"totalShares","type":"uint256"},{"indexed":false,"name":"expiryAt","type":"uint256"}],"name":"PacketCreated","type":"event"},
|
||||
{"anonymous":false,"inputs":[{"indexed":true,"name":"packetId","type":"uint256"},{"indexed":true,"name":"claimer","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"authNonce","type":"uint256"},{"indexed":false,"name":"randomSeed","type":"uint256"},{"indexed":false,"name":"blockNumber","type":"uint256"}],"name":"PacketClaimed","type":"event"},
|
||||
{"anonymous":false,"inputs":[{"indexed":true,"name":"packetId","type":"uint256"},{"indexed":true,"name":"refundTo","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"PacketRefunded","type":"event"}
|
||||
]`), nil
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// Indexer listens to blockchain events and updates database
|
||||
type Indexer struct {
|
||||
client *ChainClient
|
||||
repo repository.Repository
|
||||
pollInterval time.Duration
|
||||
lastBlock uint64
|
||||
contractAddr common.Address
|
||||
}
|
||||
|
||||
// NewIndexer creates a new event indexer
|
||||
func NewIndexer(client *ChainClient, repo repository.Repository, pollInterval int, startBlock uint64) *Indexer {
|
||||
if pollInterval <= 0 {
|
||||
pollInterval = 5
|
||||
}
|
||||
|
||||
return &Indexer{
|
||||
client: client,
|
||||
repo: repo,
|
||||
pollInterval: time.Duration(pollInterval) * time.Second,
|
||||
lastBlock: startBlock,
|
||||
contractAddr: client.contractAddr,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins polling for new events
|
||||
func (i *Indexer) Start(ctx context.Context) {
|
||||
log.Println("🚀 Starting RedPacket event indexer...")
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(i.pollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Indexer stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := i.poll(ctx); err != nil {
|
||||
log.Printf("Indexer poll error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *Indexer) poll(ctx context.Context) error {
|
||||
// Get latest block
|
||||
header, err := i.client.client.HeaderByNumber(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get header failed: %w", err)
|
||||
}
|
||||
|
||||
currentBlock := header.Number.Uint64()
|
||||
if currentBlock <= i.lastBlock {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query logs from lastBlock+1 to currentBlock
|
||||
query := ethereum.FilterQuery{
|
||||
FromBlock: big.NewInt(int64(i.lastBlock + 1)),
|
||||
ToBlock: big.NewInt(int64(currentBlock)),
|
||||
Addresses: []common.Address{i.contractAddr},
|
||||
}
|
||||
|
||||
logs, err := i.client.client.FilterLogs(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("filter logs failed: %w", err)
|
||||
}
|
||||
|
||||
// Convert to pointer slice for parser
|
||||
logPtrs := make([]*types.Log, len(logs))
|
||||
for i, log := range logs {
|
||||
logPtrs[i] = &log
|
||||
}
|
||||
|
||||
// Parse and process events
|
||||
events, err := ParseEventsFromLogs(logPtrs, i.client.contractABI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if err := i.processEvent(ctx, event, logPtrs); err != nil {
|
||||
log.Printf("Process event %s failed: %v", event.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
i.lastBlock = currentBlock
|
||||
log.Printf("✅ Indexed up to block %d, processed %d events", currentBlock, len(events))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Indexer) processEvent(ctx context.Context, event *ParsedEvent, logs []*types.Log) error {
|
||||
switch event.Name {
|
||||
case "PacketCreated":
|
||||
return i.handlePacketCreated(ctx, event)
|
||||
case "PacketClaimed":
|
||||
return i.handlePacketClaimed(ctx, event)
|
||||
case "PacketRefunded":
|
||||
return i.handlePacketRefunded(ctx, event)
|
||||
default:
|
||||
log.Printf("Unknown event: %s", event.Name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indexer) handlePacketCreated(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
creator := GetClaimerFromEvent(event) // creator is indexed as second topic
|
||||
|
||||
log.Printf("📦 PacketCreated: packetId=%s, creator=%s", packetID.String(), creator.Hex())
|
||||
|
||||
// Update database - in real implementation, link with biz_id via offchain record
|
||||
// This would typically be triggered by the created-callback first
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Indexer) handlePacketClaimed(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
claimer := GetClaimerFromEvent(event)
|
||||
amount := GetAmountFromEvent(event)
|
||||
|
||||
log.Printf("🎁 PacketClaimed: packetId=%s, claimer=%s, amount=%s",
|
||||
packetID.String(), claimer.Hex(), amount.String())
|
||||
|
||||
// Create claim record
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: packetID.String(),
|
||||
ClaimerWallet: claimer.Hex(),
|
||||
ClaimedAmount: amount.String(),
|
||||
Status: "CONFIRMED",
|
||||
}
|
||||
|
||||
return i.repo.CreateClaim(ctx, claim)
|
||||
}
|
||||
|
||||
func (i *Indexer) handlePacketRefunded(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
refundTo := GetClaimerFromEvent(event) // refundTo is indexed
|
||||
|
||||
log.Printf("♻️ PacketRefunded: packetId=%s, refundTo=%s", packetID.String(), refundTo.Hex())
|
||||
|
||||
// TODO: Update packet status to REFUNDED
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// ParsedEvent represents a parsed blockchain event
|
||||
type ParsedEvent struct {
|
||||
Name string
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
// ParseEventsFromLogs parses logs using the contract ABI
|
||||
func ParseEventsFromLogs(logs []*types.Log, contractABI abi.ABI) ([]*ParsedEvent, error) {
|
||||
var events []*ParsedEvent
|
||||
|
||||
for _, log := range logs {
|
||||
if len(log.Topics) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
event, err := parseEvent(log, contractABI)
|
||||
if err == nil && event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func parseEvent(log *types.Log, contractABI abi.ABI) (*ParsedEvent, error) {
|
||||
for name, event := range contractABI.Events {
|
||||
if event.ID != log.Topics[0] {
|
||||
continue
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
|
||||
// Parse indexed parameters from topics
|
||||
indexedIdx := 1
|
||||
for _, arg := range event.Inputs {
|
||||
if arg.Indexed {
|
||||
if indexedIdx < len(log.Topics) {
|
||||
if arg.Type.T == abi.AddressTy {
|
||||
data[arg.Name] = common.BytesToAddress(log.Topics[indexedIdx].Bytes())
|
||||
} else if arg.Type.T == abi.UintTy || arg.Type.T == abi.IntTy {
|
||||
data[arg.Name] = new(big.Int).SetBytes(log.Topics[indexedIdx].Bytes())
|
||||
} else {
|
||||
data[arg.Name] = log.Topics[indexedIdx].Hex()
|
||||
}
|
||||
indexedIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse non-indexed parameters from data
|
||||
if len(log.Data) > 0 {
|
||||
unpacked, err := event.Inputs.Unpack(log.Data)
|
||||
if err == nil {
|
||||
nonIndexedIdx := 0
|
||||
for _, arg := range event.Inputs {
|
||||
if !arg.Indexed {
|
||||
if nonIndexedIdx < len(unpacked) {
|
||||
data[arg.Name] = unpacked[nonIndexedIdx]
|
||||
nonIndexedIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ParsedEvent{
|
||||
Name: name,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown event: %s", log.Topics[0].Hex())
|
||||
}
|
||||
|
||||
// GetPacketIDFromEvent extracts packetId from event data
|
||||
func GetPacketIDFromEvent(event *ParsedEvent) *big.Int {
|
||||
if id, ok := event.Data["packetId"]; ok {
|
||||
if b, ok := id.(*big.Int); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
// GetClaimerFromEvent extracts claimer address from event
|
||||
func GetClaimerFromEvent(event *ParsedEvent) common.Address {
|
||||
if claimer, ok := event.Data["claimer"]; ok {
|
||||
if addr, ok := claimer.(common.Address); ok {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
return common.Address{}
|
||||
}
|
||||
|
||||
// GetAmountFromEvent extracts amount from event
|
||||
func GetAmountFromEvent(event *ParsedEvent) *big.Int {
|
||||
if amount, ok := event.Data["amount"]; ok {
|
||||
if b, ok := amount.(*big.Int); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return big.NewInt(0)
|
||||
}
|
||||
@ -0,0 +1,215 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// TronClient handles TRON blockchain interactions using HTTP JSON-RPC
|
||||
type TronClient struct {
|
||||
fullNodeURL string
|
||||
contractBase58 string
|
||||
ownerBase58 string
|
||||
privateKeyHex string
|
||||
feeLimit int64
|
||||
abiJSON string
|
||||
parsedABI abi.ABI
|
||||
}
|
||||
|
||||
// NewTronClient creates a new TRON client
|
||||
func NewTronClient(fullNodeURL, contractBase58, ownerBase58, privateKeyHex string, abiJSON []byte, feeLimit int64) (*TronClient, error) {
|
||||
if fullNodeURL == "" {
|
||||
return nil, fmt.Errorf("fullNodeURL is required for TRON")
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(bytes.NewReader(abiJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse TRON ABI failed: %w", err)
|
||||
}
|
||||
|
||||
return &TronClient{
|
||||
fullNodeURL: fullNodeURL,
|
||||
contractBase58: contractBase58,
|
||||
ownerBase58: ownerBase58,
|
||||
privateKeyHex: privateKeyHex,
|
||||
feeLimit: feeLimit,
|
||||
abiJSON: string(abiJSON),
|
||||
parsedABI: parsedABI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendAdminTransaction sends an admin transaction on TRON (setSigner, setToken, etc.)
|
||||
func (t *TronClient) SendAdminTransaction(ctx context.Context, methodName string, args ...interface{}) (string, error) {
|
||||
if t.privateKeyHex == "" || t.ownerBase58 == "" {
|
||||
return "", fmt.Errorf("TRON admin credentials not configured")
|
||||
}
|
||||
|
||||
// Build function selector like "setSigner(address)"
|
||||
selector := methodName
|
||||
if len(args) > 0 {
|
||||
// Simple selector generation - in production use full ABI encoding
|
||||
selector = fmt.Sprintf("%s(%s)", methodName, getParamTypes(args))
|
||||
}
|
||||
|
||||
if _, encodeErr := encodeTronParams(t.abiJSON, methodName, args...); encodeErr != nil {
|
||||
return "", fmt.Errorf("encode params failed: %w", encodeErr)
|
||||
}
|
||||
|
||||
return SendTronAdminTx(
|
||||
ctx,
|
||||
t.fullNodeURL,
|
||||
t.ownerBase58,
|
||||
t.contractBase58,
|
||||
selector,
|
||||
methodName,
|
||||
t.feeLimit,
|
||||
t.privateKeyHex,
|
||||
t.abiJSON,
|
||||
args...,
|
||||
)
|
||||
}
|
||||
|
||||
// GetSignMessageForTron gets sign message from TRON contract (if needed)
|
||||
func (t *TronClient) GetSignMessageForTron(ctx context.Context, packetID *big.Int, claimer, authNonce, randomSeed, deadline string) (string, error) {
|
||||
// TRON version would call triggersmartcontract with getSignMessage
|
||||
// For simplicity, we can reuse similar logic as ETH or implement full TRON trigger
|
||||
return "", fmt.Errorf("TRON getSignMessage not fully implemented yet - use ETH path for signing")
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func getParamTypes(args []interface{}) string {
|
||||
types := make([]string, len(args))
|
||||
for i, arg := range args {
|
||||
switch arg.(type) {
|
||||
case string, common.Address:
|
||||
types[i] = "address"
|
||||
case bool:
|
||||
types[i] = "bool"
|
||||
case int, int64, *big.Int:
|
||||
types[i] = "uint256"
|
||||
default:
|
||||
types[i] = "unknown"
|
||||
}
|
||||
}
|
||||
return strings.Join(types, ",")
|
||||
}
|
||||
|
||||
// SendTronAdminTx implements TRON transaction broadcasting (from design doc)
|
||||
func SendTronAdminTx(
|
||||
ctx context.Context,
|
||||
fullNodeURL, ownerBase58, contractBase58, selector, methodName string,
|
||||
feeLimit int64,
|
||||
privateKeyHex string,
|
||||
abiJSON string,
|
||||
args ...interface{},
|
||||
) (string, error) {
|
||||
|
||||
paramHex, err := encodeTronParams(abiJSON, methodName, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Trigger smart contract
|
||||
var triggerResp map[string]interface{}
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/triggersmartcontract", map[string]interface{}{
|
||||
"owner_address": ownerBase58,
|
||||
"contract_address": contractBase58,
|
||||
"function_selector": selector,
|
||||
"parameter": paramHex,
|
||||
"fee_limit": feeLimit,
|
||||
"call_value": 0,
|
||||
"visible": true,
|
||||
}, &triggerResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("trigger contract failed: %w", err)
|
||||
}
|
||||
|
||||
txObj, ok := triggerResp["transaction"].(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("transaction not found in trigger response")
|
||||
}
|
||||
|
||||
// Sign transaction
|
||||
var signedResp map[string]interface{}
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/gettransactionsign", map[string]interface{}{
|
||||
"transaction": txObj,
|
||||
"privateKey": privateKeyHex,
|
||||
}, &signedResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("sign transaction failed: %w", err)
|
||||
}
|
||||
|
||||
// Broadcast
|
||||
var broadcastResp map[string]interface{}
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/broadcasttransaction", signedResp, &broadcastResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("broadcast failed: %w", err)
|
||||
}
|
||||
|
||||
if result, _ := broadcastResp["result"].(bool); !result {
|
||||
return "", fmt.Errorf("broadcast failed: %v", broadcastResp)
|
||||
}
|
||||
|
||||
txid, _ := broadcastResp["txid"].(string)
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func encodeTronParams(abiJSON, method string, args ...interface{}) (string, error) {
|
||||
parsed, err := abi.JSON(strings.NewReader(abiJSON))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m, ok := parsed.Methods[method]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("method not found: %s", method)
|
||||
}
|
||||
packed, err := m.Inputs.Pack(args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(packed), nil
|
||||
}
|
||||
|
||||
func postJSON(ctx context.Context, url string, body interface{}, out interface{}) error {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("http %d: %s", resp.StatusCode, string(raw))
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,239 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
)
|
||||
|
||||
// TronIndexer provides production-grade event listening for TRON blockchain
|
||||
type TronIndexer struct {
|
||||
client *TronClient
|
||||
repo repository.Repository
|
||||
pollInterval time.Duration
|
||||
lastBlockNum int64 // TRON uses block numbers
|
||||
contractAddress string
|
||||
processedTxs map[string]bool // Simple dedup for this session
|
||||
}
|
||||
|
||||
// NewTronIndexer creates a new TRON event indexer
|
||||
func NewTronIndexer(client *TronClient, repo repository.Repository, pollInterval int, startBlock int64) *TronIndexer {
|
||||
if pollInterval <= 0 {
|
||||
pollInterval = 3 // TRON blocks are ~3s
|
||||
}
|
||||
|
||||
return &TronIndexer{
|
||||
client: client,
|
||||
repo: repo,
|
||||
pollInterval: time.Duration(pollInterval) * time.Second,
|
||||
lastBlockNum: startBlock,
|
||||
contractAddress: client.contractBase58,
|
||||
processedTxs: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins polling for TRON blockchain events
|
||||
func (t *TronIndexer) Start(ctx context.Context) {
|
||||
log.Println("🚀 Starting TRON event indexer... (Production mode)")
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(t.pollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("TRON Indexer stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := t.poll(ctx); err != nil {
|
||||
log.Printf("TRON Indexer poll error: %v", err)
|
||||
// Backoff on error
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *TronIndexer) poll(ctx context.Context) error {
|
||||
// Get current block
|
||||
currentBlock, err := t.getNowBlock(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get now block failed: %w", err)
|
||||
}
|
||||
|
||||
if currentBlock <= t.lastBlockNum {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("📡 TRON scanning blocks %d to %d", t.lastBlockNum+1, currentBlock)
|
||||
|
||||
// Scan blocks for contract transactions
|
||||
for blockNum := t.lastBlockNum + 1; blockNum <= currentBlock; blockNum++ {
|
||||
if err := t.scanBlock(ctx, blockNum); err != nil {
|
||||
log.Printf("Warning: failed to scan TRON block %d: %v", blockNum, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
t.lastBlockNum = currentBlock
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TronIndexer) getNowBlock(ctx context.Context) (int64, error) {
|
||||
var resp map[string]interface{}
|
||||
err := postJSON(ctx, t.client.fullNodeURL+"/wallet/getnowblock", map[string]interface{}{}, &resp)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if blockHeader, ok := resp["block_header"].(map[string]interface{}); ok {
|
||||
if rawData, ok := blockHeader["raw_data"].(map[string]interface{}); ok {
|
||||
if number, ok := rawData["number"].(float64); ok {
|
||||
return int64(number), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("could not parse block number")
|
||||
}
|
||||
|
||||
func (t *TronIndexer) scanBlock(ctx context.Context, blockNum int64) error {
|
||||
// Get block by number
|
||||
var blockResp map[string]interface{}
|
||||
err := postJSON(ctx, t.client.fullNodeURL+"/wallet/getblockbynum", map[string]interface{}{
|
||||
"num": blockNum,
|
||||
}, &blockResp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transactions, ok := blockResp["transactions"].([]interface{})
|
||||
if !ok {
|
||||
return nil // no transactions
|
||||
}
|
||||
|
||||
for _, txInterface := range transactions {
|
||||
tx, ok := txInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
txID, _ := tx["txID"].(string)
|
||||
if txID == "" || t.processedTxs[txID] {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := t.processTransaction(ctx, txID); err != nil {
|
||||
log.Printf("Failed to process TRON tx %s: %v", txID, err)
|
||||
} else {
|
||||
t.processedTxs[txID] = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TronIndexer) processTransaction(ctx context.Context, txID string) error {
|
||||
// Get transaction info with logs
|
||||
var txInfo map[string]interface{}
|
||||
err := postJSON(ctx, t.client.fullNodeURL+"/wallet/gettransactioninfobyid", map[string]interface{}{
|
||||
"value": txID,
|
||||
}, &txInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if this transaction interacted with our contract
|
||||
contractAddress := t.client.contractBase58
|
||||
if logs, ok := txInfo["log"].([]interface{}); ok && len(logs) > 0 {
|
||||
for _, logEntry := range logs {
|
||||
if logMap, ok := logEntry.(map[string]interface{}); ok {
|
||||
if address, ok := logMap["address"].(string); ok && address == contractAddress {
|
||||
// This is our contract event
|
||||
eventType := t.parseTronEvent(logMap)
|
||||
log.Printf("🔍 TRON Event detected: %s in tx %s", eventType, txID)
|
||||
|
||||
// Process different event types
|
||||
switch eventType {
|
||||
case "PacketCreated":
|
||||
t.handleTronPacketCreated(ctx, logMap, txID)
|
||||
case "PacketClaimed":
|
||||
t.handleTronPacketClaimed(ctx, logMap, txID)
|
||||
case "PacketRefunded":
|
||||
t.handleTronPacketRefunded(ctx, logMap, txID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TronIndexer) parseTronEvent(logEntry map[string]interface{}) string {
|
||||
// TRON events are more complex. In production, you'd decode topics and data
|
||||
// For this implementation, we use a simplified approach based on log data
|
||||
if topics, ok := logEntry["topics"].([]interface{}); ok && len(topics) > 0 {
|
||||
if topic0, ok := topics[0].(string); ok {
|
||||
// Map common TRON event signatures (this would be expanded with real contract event IDs)
|
||||
switch topic0 {
|
||||
case "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0": // Transfer (example)
|
||||
return "Transfer"
|
||||
// Add real RedPacket event signatures here from contract
|
||||
default:
|
||||
return "UnknownEvent"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "UnknownEvent"
|
||||
}
|
||||
|
||||
// Event handlers - these would update the database with parsed event data
|
||||
|
||||
func (t *TronIndexer) handleTronPacketCreated(ctx context.Context, logData map[string]interface{}, txID string) {
|
||||
log.Printf("📦 [TRON] PacketCreated event in tx %s", txID)
|
||||
// TODO: Parse packetId, creator, amount, etc. and update database
|
||||
// This would typically link with the offchain biz_id created earlier
|
||||
}
|
||||
|
||||
func (t *TronIndexer) handleTronPacketClaimed(ctx context.Context, logData map[string]interface{}, txID string) {
|
||||
log.Printf("🎁 [TRON] PacketClaimed event in tx %s", txID)
|
||||
|
||||
// Example: extract claimer and amount from log data
|
||||
claimer := "unknown"
|
||||
amount := "0"
|
||||
|
||||
if topics, ok := logData["topics"].([]interface{}); ok && len(topics) > 1 {
|
||||
if claimerTopic, ok := topics[1].(string); ok {
|
||||
claimer = claimerTopic // simplified
|
||||
}
|
||||
}
|
||||
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: "tron-packet-" + txID[:8], // placeholder
|
||||
ClaimerWallet: claimer,
|
||||
ClaimTxHash: txID,
|
||||
ClaimedAmount: amount,
|
||||
Status: "CONFIRMED",
|
||||
}
|
||||
|
||||
if err := t.repo.CreateClaim(ctx, claim); err != nil {
|
||||
log.Printf("Failed to save TRON claim: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TronIndexer) handleTronPacketRefunded(ctx context.Context, logData map[string]interface{}, txID string) {
|
||||
log.Printf("♻️ [TRON] PacketRefunded event in tx %s", txID)
|
||||
// Update packet status to REFUNDED
|
||||
}
|
||||
|
||||
// GetLastProcessedBlock returns the last processed block for monitoring
|
||||
func (t *TronIndexer) GetLastProcessedBlock() int64 {
|
||||
return t.lastBlockNum
|
||||
}
|
||||
@ -0,0 +1,134 @@
|
||||
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)
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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) {
|
||||
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) {
|
||||
var req struct {
|
||||
PacketID string `json:"packet_id" binding:"required"`
|
||||
Claimer string `json:"claimer" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
RandomSeed string `json:"random_seed"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.rpSvc.CanClaim(c.Request.Context(), req.PacketID, req.Claimer, req.UserID); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.IssueClaimSign(c.Request.Context(), req.PacketID, req.Claimer, req.UserID, 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) {
|
||||
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})
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RedPacket struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
BizID string `gorm:"uniqueIndex;size:64" json:"biz_id"`
|
||||
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"`
|
||||
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"`
|
||||
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;size:32" json:"packet_id"`
|
||||
ClaimerWallet string `gorm:"size:66" json:"claimer_wallet"`
|
||||
AuthNonce string `gorm:"size:32" json:"auth_nonce"`
|
||||
ClaimTxHash string `gorm:"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:"size:66" json:"tx_hash"`
|
||||
Amount string `gorm:"size:50" json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"redpacket/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
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)
|
||||
UpdateRedPacketTxHash(ctx context.Context, bizID, txHash, packetID 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
|
||||
CreateClaim(ctx context.Context, claim *model.RedPacketClaim) error
|
||||
GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, 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) UpdateRedPacketTxHash(ctx context.Context, bizID, txHash, packetID string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacket{}).
|
||||
Where("biz_id = ?", bizID).
|
||||
Updates(map[string]interface{}{
|
||||
"tx_hash": txHash,
|
||||
"packet_id": packetID,
|
||||
"status": "ACTIVE",
|
||||
}).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) CreateClaim(ctx context.Context, claim *model.RedPacketClaim) error {
|
||||
return r.db.WithContext(ctx).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
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
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)
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"redpacket/internal/chain"
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RedPacketService struct {
|
||||
repo repository.Repository
|
||||
chainClient *chain.ChainClient
|
||||
signerKey *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
type CreateOrderRequest struct {
|
||||
CreatorUserID string `json:"creator_user_id" binding:"required"`
|
||||
CreatorWallet string `json:"creator_wallet" binding:"required"`
|
||||
PacketType int32 `json:"packet_type" binding:"required"`
|
||||
Token string `json:"token"`
|
||||
TotalAmount string `json:"total_amount" binding:"required"`
|
||||
TotalShares int32 `json:"total_shares" binding:"required"`
|
||||
ExpiryAt int64 `json:"expiry_at"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type CreatedCallbackRequest struct {
|
||||
BizID string `json:"biz_id" binding:"required"`
|
||||
TxHash string `json:"tx_hash" binding:"required"`
|
||||
PacketID string `json:"packet_id" binding:"required"`
|
||||
}
|
||||
|
||||
type ClaimResultRequest struct {
|
||||
PacketID string `json:"packet_id" binding:"required"`
|
||||
Claimer string `json:"claimer" binding:"required"`
|
||||
UserID string `json:"user_id"`
|
||||
TxHash string `json:"tx_hash" binding:"required"`
|
||||
}
|
||||
|
||||
func NewRedPacketService(repo repository.Repository, chainClient *chain.ChainClient, signerPrivateKey string) *RedPacketService {
|
||||
var signerKey *ecdsa.PrivateKey
|
||||
if signerPrivateKey != "" {
|
||||
var err error
|
||||
signerKey, err = crypto.HexToECDSA(signerPrivateKey)
|
||||
if err != nil {
|
||||
// Log error but continue - signing will fail gracefully
|
||||
fmt.Printf("Warning: failed to parse signer private key: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &RedPacketService{
|
||||
repo: repo,
|
||||
chainClient: chainClient,
|
||||
signerKey: signerKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RedPacketService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (map[string]interface{}, error) {
|
||||
bizID := uuid.NewString()
|
||||
|
||||
rp := &model.RedPacket{
|
||||
BizID: bizID,
|
||||
CreatorUserID: req.CreatorUserID,
|
||||
CreatorWallet: req.CreatorWallet,
|
||||
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.repo.CreateRedPacket(ctx, rp); err != nil {
|
||||
return nil, fmt.Errorf("failed to create red packet: %w", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"biz_id": bizID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RedPacketService) CreatedCallback(ctx context.Context, req *CreatedCallbackRequest) error {
|
||||
return s.repo.UpdateRedPacketTxHash(ctx, req.BizID, req.TxHash, req.PacketID)
|
||||
}
|
||||
|
||||
func (s *RedPacketService) GetDetail(ctx context.Context, packetID string) (map[string]interface{}, error) {
|
||||
rp, err := s.repo.GetRedPacketByPacketID(ctx, packetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("packet not found: %s", packetID)
|
||||
}
|
||||
|
||||
claims, err := s.repo.GetClaimsByPacketID(ctx, packetID)
|
||||
if err != nil {
|
||||
claims = []model.RedPacketClaim{}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"biz_record": rp,
|
||||
"claims": claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RedPacketService) CanClaim(ctx context.Context, packetID, claimer, userID string) error {
|
||||
// Check if packet exists and is active
|
||||
rp, err := s.repo.GetRedPacketByPacketID(ctx, packetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("packet not found: %s", packetID)
|
||||
}
|
||||
|
||||
if rp.Status != "ACTIVE" {
|
||||
return fmt.Errorf("packet is not active, current status: %s", rp.Status)
|
||||
}
|
||||
|
||||
// TODO: Add more checks - expiry, already claimed by this user, etc.
|
||||
// For now we allow the claim
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignClaim generates signature for claim operation
|
||||
func (s *RedPacketService) IssueClaimSign(ctx context.Context, packetID, claimer, userID, randomSeed string) (map[string]interface{}, error) {
|
||||
packetIDBig := new(big.Int)
|
||||
packetIDBig.SetString(packetID, 10)
|
||||
|
||||
claimerAddr := common.HexToAddress(claimer)
|
||||
|
||||
// Generate nonce and deadline (5 minute expiry)
|
||||
nonce := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
deadline := time.Now().Add(5 * time.Minute).Unix()
|
||||
randomSeedBig := new(big.Int)
|
||||
if randomSeed != "" && randomSeed != "0" {
|
||||
randomSeedBig.SetString(randomSeed, 10)
|
||||
} else {
|
||||
randomSeedBig.SetInt64(time.Now().UnixNano())
|
||||
}
|
||||
deadlineBig := big.NewInt(deadline)
|
||||
|
||||
var digest [32]byte
|
||||
var err error
|
||||
|
||||
if s.chainClient != nil {
|
||||
// Use real contract call to getSignMessage
|
||||
digest, err = s.chainClient.GetSignMessage(ctx, packetIDBig, claimerAddr, big.NewInt(0), randomSeedBig, deadlineBig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getSignMessage failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Fallback for testing
|
||||
digest = crypto.Keccak256Hash([]byte(fmt.Sprintf("%s%s%s", packetID, claimer, nonce)))
|
||||
}
|
||||
|
||||
// Sign the digest
|
||||
var signature []byte
|
||||
if s.signerKey != nil {
|
||||
signature, err = crypto.Sign(digest[:], s.signerKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign failed: %w", err)
|
||||
}
|
||||
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: packetID,
|
||||
Claimer: claimer,
|
||||
AuthNonce: nonce,
|
||||
RandomSeed: randomSeedBig.String(),
|
||||
Deadline: deadline,
|
||||
Signature: sigHex,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.repo.CreateClaimAuth(ctx, auth); err != nil {
|
||||
return nil, fmt.Errorf("save claim auth failed: %w", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"auth_nonce": nonce,
|
||||
"deadline": deadline,
|
||||
"signature": sigHex,
|
||||
"random_seed": randomSeedBig.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RedPacketService) ClaimResult(ctx context.Context, req *ClaimResultRequest) error {
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: req.PacketID,
|
||||
ClaimerWallet: req.Claimer,
|
||||
ClaimTxHash: req.TxHash,
|
||||
Status: "CONFIRMED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
return s.repo.CreateClaim(ctx, claim)
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
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"
|
||||
)
|
||||
|
||||
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{},
|
||||
); 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 repository and service
|
||||
repo := repository.New(db)
|
||||
rpSvc := service.NewRedPacketService(repo, chainClient, cfg.Chain.SignerPrivateKey)
|
||||
|
||||
// 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 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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
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.
@ -0,0 +1,34 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue