pull/3727/head
hawklin2017 1 month ago
parent 392943654b
commit 541471f401

@ -1,112 +1,92 @@
# RedPacket Backend Service
# RedPacket RPC Service
A Web3 Red Packet service supporting Ethereum and TRON, following the design documents:
A Web3 Red Packet RPC service that has been migrated to the standard OpenIM
service layout: gRPC over `protocol/redpacket`, MongoDB via the `mgo` +
`controller` pattern, and command/discovery wiring through `pkg/common/cmd`
and `pkg/common/startrpc`.
- `backend-api.md` - API specifications
- `client-integration-guide.md` - Frontend / gateway integration guide
- `redpacket-web3-integration-design.md` - Architecture and flows
- `red-packet-go-backend-eth-tron.md` - Blockchain integration details
For HTTP access, the service is exposed by the API gateway under `/redpacket/*`
(see `internal/api/redpacket.go`).
## 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
- ✅ EVM signature generation via `getSignMessage(...)`
- ✅ Basic EVM event indexing for claim/refund synchronization
- ✅ Idempotent claim/refund persistence by transaction hash
- ✅ Admin configuration endpoints
## Current Status
This service is runnable and suitable for continued iteration, but it is not yet fully production-complete.
Working well now:
- EVM-side claim signing uses the real `authNonce` in the digest
- Claim pre-checks cover packet existence, active status, expiry, and already-claimed cases
- EVM ABI and event parsing are aligned with the current contract events
- Claim and refund events can be persisted idempotently
Still incomplete:
- ETH admin endpoints are still mostly mock behavior
- `PacketCreated` indexing is not yet fully wired for automatic order reconciliation
- TRON `getSignMessage` flow is not complete
- TRON event decoding is still a scaffold
- Admin APIs still need authentication and audit controls
## 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
## Layout
# 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
}'
.
├── main.go # cmd.NewRedPacketRpcCmd().Exec()
├── README.md
├── backend-api.md # Legacy API docs, kept for reference
├── client-integration-guide.md # Legacy integration docs, kept for reference
├── red-packet-go-backend-eth-tron.md # Architecture / chain integration design
└── redpacket-web3-integration-design.md # Web3 integration design
```
## Project Structure
The actual implementation lives in:
```
.
├── config/ # Configuration
├── internal/
│ ├── handler/ # HTTP handlers (Gin)
│ ├── model/ # Database models (GORM)
│ ├── repository/ # Data access layer
│ ├── service/ # Business logic
│ └── chain/ # Blockchain integration and event indexing
├── pkg/resp/ # Response helpers
├── router/ # Route definitions
├── main.go
├── go.mod
└── README.md
```
- `protocol/redpacket/redpacket.proto` gRPC contract
- `pkg/common/storage/model/redpacket.go` Mongo BSON models
- `pkg/common/storage/database/redpacket.go` DAO interfaces
- `pkg/common/storage/database/mgo/redpacket.go` Mongo DAO impl
- `pkg/common/storage/controller/redpacket.go` Aggregated database façade
- `pkg/common/cmd/rpc_redpacket.go` Cobra entry, startrpc bootstrap
- `internal/rpc/redpacket/` gRPC service, chain client, indexers
- `internal/api/redpacket.go` Gin gateway handlers
- `config/openim-rpc-redpacket.yml` Service configuration
## Recommended Next Steps
## Features
1. Implement real ETH admin transactions for signer/token/expiry configuration
2. Finish `PacketCreated` indexing and automatic order reconciliation
3. Complete TRON `getSignMessage` and reliable event decoding
4. Add authentication, audit, and rate limiting for sensitive endpoints
5. Extend end-to-end test coverage
- ✅ Create red packet orders + on-chain `Created` callback reconciliation
- ✅ Red packet detail query (with full claim history)
- ✅ Claim signature issuance using the contract's `getSignMessage(...)`
- ✅ Claim result reporting + idempotent persistence by tx hash
- ✅ EVM event indexer (claim / refund)
- ✅ TRON full-node JSON-RPC integration scaffold
- ✅ EVM SIWE-style wallet binding (challenge / sign / confirm)
- ✅ Admin endpoints (signer / allowed token / expiry / allow-all-tokens / native-token)
## Configuration
See `config/openim-rpc-redpacket.yml` (alongside other OpenIM RPC configs).
```yaml
rpc:
registerIP: ""
listenIP: 0.0.0.0
autoSetPorts: false
ports: [10560]
prometheus:
enable: false
ports: [12560]
chain: # Optional — leave rpcURL empty to disable EVM
rpcURL: ""
contractAddress: ""
chainID: 0
signerPrivateKey: ""
configAdminPrivateKey: ""
tron: # Optional — leave fullNodeURL empty to disable TRON
fullNodeURL: ""
contractBase58: ""
ownerBase58: ""
privateKeyHex: ""
feeLimit: 100000000
indexer:
pollInterval: 5
```
See the three design documents for detailed specifications.
`config/share.yml` registers the service name as `redPacket`.
## API Documentation
## Limitations / TODO
See:
- TRON `ConfirmWalletBind` signature verification is not yet implemented and
returns `not implemented`.
- TRON event decoding in `chain/tron_indexer.go` is still a scaffold and only
identifies events by topic-0; payload decoding will be added once the
contract event signatures are finalized.
- Admin endpoints (`/redpacket/admin/*`) currently mirror the legacy mock
behaviour for EVM and only forward live calls on TRON.
- `backend-api.md` for complete API reference with request / response examples
- `client-integration-guide.md` for frontend, wallet-binding, and claim-sign integration steps
See `backend-api.md`, `client-integration-guide.md`, and the design docs for
detailed specifications.

@ -1,71 +0,0 @@
package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
Server struct {
Port int `yaml:"port"`
} `yaml:"server"`
DB struct {
Driver string `yaml:"driver"`
DSN string `yaml:"dsn"`
} `yaml:"db"`
Chain struct {
RPCURL string `yaml:"rpc_url"`
ContractAddress string `yaml:"contract_address"`
ChainID int64 `yaml:"chain_id"`
SignerPrivateKey string `yaml:"signer_private_key"`
ConfigAdminPrivateKey string `yaml:"config_admin_private_key"`
} `yaml:"chain"`
Tron struct {
FullNodeURL string `yaml:"full_node_url"`
ContractBase58 string `yaml:"contract_base58"`
OwnerBase58 string `yaml:"owner_base58"`
PrivateKeyHex string `yaml:"private_key_hex"`
FeeLimit int64 `yaml:"fee_limit"`
} `yaml:"tron"`
Indexer struct {
PollInterval int `yaml:"poll_interval"`
} `yaml:"indexer"`
}
var Cfg Config
// Load loads configuration from YAML file
func Load(configPath string) {
if configPath == "" {
configPath = "config/config.yaml"
}
data, err := os.ReadFile(configPath)
if err != nil {
fmt.Printf("Warning: could not read config file %s: %v, using defaults\n", configPath, err)
setDefaults()
return
}
if err := yaml.Unmarshal(data, &Cfg); err != nil {
fmt.Printf("Warning: could not parse config: %v, using defaults\n", err)
setDefaults()
return
}
fmt.Printf("Loaded config from %s\n", configPath)
}
func setDefaults() {
Cfg.Server.Port = 8080
Cfg.DB.Driver = "sqlite"
Cfg.DB.DSN = "redpacket.db"
Cfg.Chain.ChainID = 1
Cfg.Indexer.PollInterval = 5
}

@ -1,23 +0,0 @@
server:
port: 8080
db:
driver: sqlite
dsn: redpacket.db
chain:
rpc_url: "https://eth.llamarpc.com"
contract_address: "0xYourRedPacketContractAddress"
chain_id: 1
signer_private_key: "your-signer-private-key-here"
config_admin_private_key: "your-config-admin-private-key-here"
tron:
full_node_url: ""
contract_base58: ""
owner_base58: ""
private_key_hex: ""
fee_limit: 10000000000
indexer:
poll_interval: 5

@ -1,68 +0,0 @@
module redpacket
go 1.22
require (
github.com/ethereum/go-ethereum v1.14.12
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/supranational/blst v0.3.13 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

@ -1,260 +0,0 @@
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M=
github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I=
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs=
github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI=
github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4=
github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY=
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A=
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk=
github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

@ -1,49 +0,0 @@
package authctx
import (
"context"
"fmt"
"strings"
"github.com/gin-gonic/gin"
)
const opUserIDKey = "opUserID"
type userIDContextKey struct{}
func WithCurrentUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDContextKey{}, strings.TrimSpace(userID))
}
func CurrentUserID(ctx context.Context) (string, error) {
if ctx == nil {
return "", fmt.Errorf("request context is nil")
}
if userID, ok := ctx.Value(userIDContextKey{}).(string); ok && strings.TrimSpace(userID) != "" {
return strings.TrimSpace(userID), nil
}
if userID, ok := ctx.Value(opUserIDKey).(string); ok && strings.TrimSpace(userID) != "" {
return strings.TrimSpace(userID), nil
}
return "", fmt.Errorf("op user id missing in context")
}
func BindCurrentUserID(c *gin.Context) error {
if c == nil {
return fmt.Errorf("gin context is nil")
}
userID := strings.TrimSpace(c.GetString(opUserIDKey))
if userID == "" {
if value := c.Request.Context().Value(opUserIDKey); value != nil {
if fromCtx, ok := value.(string); ok {
userID = strings.TrimSpace(fromCtx)
}
}
}
if userID == "" {
return fmt.Errorf("op user id missing in context")
}
c.Request = c.Request.WithContext(WithCurrentUserID(c.Request.Context(), userID))
return nil
}

@ -1,134 +0,0 @@
package handler
import (
"redpacket/internal/service"
"redpacket/pkg/resp"
"github.com/gin-gonic/gin"
)
type AdminHandler struct {
adminSvc *service.AdminService
}
func NewAdminHandler(adminSvc *service.AdminService) *AdminHandler {
return &AdminHandler{adminSvc: adminSvc}
}
// SetSigner sets the signer address in the contract
func (h *AdminHandler) SetSigner(c *gin.Context) {
var req struct {
SignerAddress string `json:"signer_address" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
if err := h.adminSvc.SetSigner(c.Request.Context(), req.SignerAddress); err != nil {
resp.InternalError(c, "failed to set signer: "+err.Error())
return
}
resp.OK(c, gin.H{"message": "signer address updated successfully"})
}
// SetToken configures allowed token
func (h *AdminHandler) SetToken(c *gin.Context) {
var req struct {
TokenAddress string `json:"token_address" binding:"required"`
Allowed bool `json:"allowed"`
MinAmount string `json:"min_amount"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
if err := h.adminSvc.SetToken(c.Request.Context(), req.TokenAddress, req.Allowed, req.MinAmount); err != nil {
resp.InternalError(c, "failed to set token: "+err.Error())
return
}
resp.OK(c, gin.H{"message": "token configuration updated"})
}
// SetExpiry sets default expiry duration
func (h *AdminHandler) SetExpiry(c *gin.Context) {
var req struct {
ExpirySeconds int64 `json:"expiry_seconds" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
if err := h.adminSvc.SetExpiry(c.Request.Context(), req.ExpirySeconds); err != nil {
resp.InternalError(c, "failed to set expiry: "+err.Error())
return
}
resp.OK(c, gin.H{"message": "expiry duration updated"})
}
// SetAllowAllTokens sets whether all tokens are allowed
func (h *AdminHandler) SetAllowAllTokens(c *gin.Context) {
var req struct {
AllowAll bool `json:"allow_all"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
if err := h.adminSvc.SetAllowAllTokens(c.Request.Context(), req.AllowAll); err != nil {
resp.InternalError(c, "failed to update allow all tokens: "+err.Error())
return
}
resp.OK(c, gin.H{"message": "allow all tokens setting updated"})
}
// SetNativeTokenEnabled enables/disables native token (ETH/TRX)
func (h *AdminHandler) SetNativeTokenEnabled(c *gin.Context) {
var req struct {
Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
if err := h.adminSvc.SetNativeTokenEnabled(c.Request.Context(), req.Enabled); err != nil {
resp.InternalError(c, "failed to update native token setting: "+err.Error())
return
}
resp.OK(c, gin.H{"message": "native token setting updated"})
}
// ParseTxEvents manually parses events from a transaction hash (for debugging)
func (h *AdminHandler) ParseTxEvents(c *gin.Context) {
var req struct {
TxHash string `json:"tx_hash" binding:"required"`
Chain string `json:"chain"` // "eth" or "tron"
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
result, err := h.adminSvc.ParseTxEvents(c.Request.Context(), req.TxHash, req.Chain)
if err != nil {
resp.InternalError(c, "failed to parse tx events: "+err.Error())
return
}
resp.OK(c, result)
}

@ -1,176 +0,0 @@
package handler
import (
"net/http"
"redpacket/internal/authctx"
"redpacket/internal/service"
"redpacket/pkg/resp"
"github.com/gin-gonic/gin"
)
type RedPacketHandler struct {
rpSvc *service.RedPacketService
}
func NewRedPacketHandler(rpSvc *service.RedPacketService) *RedPacketHandler {
return &RedPacketHandler{rpSvc: rpSvc}
}
func (h *RedPacketHandler) CreateOrder(c *gin.Context) {
if err := authctx.BindCurrentUserID(c); err != nil {
resp.Forbidden(c, err.Error())
return
}
var req service.CreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
result, err := h.rpSvc.CreateOrder(c.Request.Context(), &req)
if err != nil {
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
return
}
resp.OK(c, result)
}
func (h *RedPacketHandler) CreatedCallback(c *gin.Context) {
var req service.CreatedCallbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
if err := h.rpSvc.CreatedCallback(c.Request.Context(), &req); err != nil {
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
return
}
resp.OK(c, gin.H{"ok": true})
}
func (h *RedPacketHandler) Detail(c *gin.Context) {
packetID := c.Query("packet_id")
if packetID == "" {
resp.BadRequest(c, "packet_id is required")
return
}
detail, err := h.rpSvc.GetDetail(c.Request.Context(), packetID)
if err != nil {
resp.Fail(c, http.StatusNotFound, 404, err.Error())
return
}
resp.OK(c, detail)
}
func (h *RedPacketHandler) ClaimSign(c *gin.Context) {
if err := authctx.BindCurrentUserID(c); err != nil {
resp.Forbidden(c, err.Error())
return
}
var req struct {
PacketID string `json:"packet_id" binding:"required"`
Claimer string `json:"claimer" binding:"required"`
RandomSeed string `json:"random_seed"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
result, err := h.rpSvc.IssueClaimSign(c.Request.Context(), req.PacketID, req.Claimer, req.RandomSeed)
if err != nil {
resp.InternalError(c, "failed to issue claim signature: "+err.Error())
return
}
resp.OK(c, result)
}
func (h *RedPacketHandler) ClaimResult(c *gin.Context) {
if err := authctx.BindCurrentUserID(c); err != nil {
resp.Forbidden(c, err.Error())
return
}
var req service.ClaimResultRequest
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
if err := h.rpSvc.ClaimResult(c.Request.Context(), &req); err != nil {
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
return
}
resp.OK(c, gin.H{"ok": true})
}
func (h *RedPacketHandler) WalletBindChallenge(c *gin.Context) {
if err := authctx.BindCurrentUserID(c); err != nil {
resp.Forbidden(c, err.Error())
return
}
var req service.WalletBindChallengeRequest
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
result, err := h.rpSvc.IssueWalletBindChallenge(c.Request.Context(), &req)
if err != nil {
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
return
}
resp.OK(c, result)
}
func (h *RedPacketHandler) WalletBindConfirm(c *gin.Context) {
var req service.WalletBindConfirmRequest
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "invalid request body: "+err.Error())
return
}
result, err := h.rpSvc.ConfirmWalletBind(c.Request.Context(), &req)
if err != nil {
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
return
}
resp.OK(c, result)
}
func (h *RedPacketHandler) WalletBindDetail(c *gin.Context) {
if err := authctx.BindCurrentUserID(c); err != nil {
resp.Forbidden(c, err.Error())
return
}
chainType := c.Query("chain_type")
walletAddress := c.Query("wallet_address")
if chainType == "" || walletAddress == "" {
resp.BadRequest(c, "chain_type and wallet_address are required")
return
}
result, err := h.rpSvc.GetWalletBinding(c.Request.Context(), "", chainType, walletAddress)
if err != nil {
resp.Fail(c, http.StatusNotFound, 404, err.Error())
return
}
resp.OK(c, result)
}

@ -1,99 +0,0 @@
package model
import (
"time"
)
type RedPacket struct {
ID uint `gorm:"primarykey" json:"id"`
BizID string `gorm:"uniqueIndex;size:64" json:"biz_id"`
ChainType string `gorm:"index;size:16" json:"chain_type"` // EVM, TRON
PacketID string `gorm:"index;size:32" json:"packet_id"`
ChainID int64 `json:"chain_id"`
ContractAddress string `json:"contract_address"`
CreatorUserID string `gorm:"size:64" json:"creator_user_id"`
CreatorWallet string `gorm:"size:66" json:"creator_wallet"`
GroupID string `gorm:"index;size:64" json:"group_id"`
ScopeType string `gorm:"size:20" json:"scope_type"` // GROUP, DIRECT, PUBLIC
ReceiverUserID string `gorm:"size:64" json:"receiver_user_id"`
ReceiverUserIDs string `gorm:"type:text" json:"receiver_user_ids"`
PacketType int32 `json:"packet_type"` // 0=fixed, 1=random, 2=transfer
Token string `gorm:"size:66" json:"token"`
TotalAmount string `gorm:"size:50" json:"total_amount"`
TotalShares int32 `json:"total_shares"`
ClaimedAmount string `gorm:"size:50" json:"claimed_amount"`
ClaimedShares int32 `json:"claimed_shares"`
ExpiryAt int64 `json:"expiry_at"`
TxHash string `gorm:"size:66" json:"tx_hash"`
Status string `gorm:"size:20" json:"status"` // PENDING, ACTIVE, EXPIRED, COMPLETED, REFUNDED
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RedPacketClaim struct {
ID uint `gorm:"primarykey" json:"id"`
PacketID string `gorm:"index;index:idx_packet_user;size:32" json:"packet_id"`
UserID string `gorm:"index;index:idx_packet_user;size:64" json:"user_id"`
ClaimerWallet string `gorm:"size:66" json:"claimer_wallet"`
AuthNonce string `gorm:"size:32" json:"auth_nonce"`
ClaimTxHash string `gorm:"uniqueIndex;size:66" json:"claim_tx_hash"`
ClaimedAmount string `gorm:"size:50" json:"claimed_amount"`
BlockNumber uint64 `json:"block_number"`
Status string `gorm:"size:20" json:"status"` // PENDING, CONFIRMED, FAILED
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RedPacketClaimAuth struct {
ID uint `gorm:"primarykey" json:"id"`
PacketID string `gorm:"index;size:32" json:"packet_id"`
Claimer string `gorm:"size:66" json:"claimer"`
AuthNonce string `gorm:"uniqueIndex;size:32" json:"auth_nonce"`
RandomSeed string `gorm:"size:32" json:"random_seed"`
Deadline int64 `json:"deadline"`
Signature string `gorm:"size:132" json:"signature"`
Used bool `json:"used"`
CreatedAt time.Time `json:"created_at"`
}
type RedPacketRefund struct {
ID uint `gorm:"primarykey" json:"id"`
PacketID string `gorm:"index;size:32" json:"packet_id"`
RefundTo string `gorm:"size:66" json:"refund_to"`
TxHash string `gorm:"uniqueIndex;size:66" json:"tx_hash"`
Amount string `gorm:"size:50" json:"amount"`
CreatedAt time.Time `json:"created_at"`
}
type WalletBindingChallenge struct {
ID uint `gorm:"primarykey" json:"id"`
ChallengeID string `gorm:"uniqueIndex;size:64" json:"challenge_id"`
UserID string `gorm:"index;size:64" json:"user_id"`
ChainType string `gorm:"index;size:16" json:"chain_type"`
ChainID int64 `json:"chain_id"`
WalletAddress string `gorm:"index;size:128" json:"wallet_address"`
Nonce string `gorm:"size:64" json:"nonce"`
Message string `gorm:"type:text" json:"message"`
Protocol string `gorm:"size:32" json:"protocol"`
SignMethod string `gorm:"size:32" json:"sign_method"`
Status string `gorm:"size:20" json:"status"` // PENDING, VERIFIED, EXPIRED, FAILED
Signature string `gorm:"type:text" json:"signature"`
ExpiresAt time.Time `json:"expires_at"`
VerifiedAt *time.Time `json:"verified_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WalletBinding struct {
ID uint `gorm:"primarykey" json:"id"`
UserID string `gorm:"index:idx_user_chain_wallet,unique;size:64" json:"user_id"`
ChainType string `gorm:"index:idx_user_chain_wallet,unique;size:16" json:"chain_type"`
ChainID int64 `json:"chain_id"`
WalletAddress string `gorm:"index:idx_user_chain_wallet,unique;size:128" json:"wallet_address"`
Status string `gorm:"size:20" json:"status"` // ACTIVE, REVOKED
ChallengeID string `gorm:"size:64" json:"challenge_id"`
VerifiedAt time.Time `json:"verified_at"`
RevokedAt *time.Time `json:"revoked_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

@ -1,251 +0,0 @@
package repository
import (
"context"
"math/big"
"redpacket/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Repository interface {
CreateRedPacket(ctx context.Context, rp *model.RedPacket) error
GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error)
GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error)
UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error
UpdateRedPacketStatus(ctx context.Context, packetID, status string) error
UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error
CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error
GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error)
MarkClaimAuthUsed(ctx context.Context, authNonce string) error
GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error)
GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error)
SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error
GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, error)
SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error
CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error)
UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error
GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error)
}
type repository struct {
db *gorm.DB
}
func New(db *gorm.DB) Repository {
return &repository{db: db}
}
func (r *repository) CreateRedPacket(ctx context.Context, rp *model.RedPacket) error {
return r.db.WithContext(ctx).Create(rp).Error
}
func (r *repository) GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error) {
var rp model.RedPacket
err := r.db.WithContext(ctx).Where("biz_id = ?", bizID).First(&rp).Error
return &rp, err
}
func (r *repository) GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error) {
var rp model.RedPacket
err := r.db.WithContext(ctx).Where("packet_id = ?", packetID).First(&rp).Error
return &rp, err
}
func (r *repository) UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error {
return r.db.WithContext(ctx).Model(&model.RedPacket{}).
Where("biz_id = ?", rp.BizID).
Updates(map[string]interface{}{
"chain_type": rp.ChainType,
"packet_id": rp.PacketID,
"tx_hash": rp.TxHash,
"chain_id": rp.ChainID,
"contract_address": rp.ContractAddress,
"group_id": rp.GroupID,
"scope_type": rp.ScopeType,
"receiver_user_id": rp.ReceiverUserID,
"receiver_user_ids": rp.ReceiverUserIDs,
"status": rp.Status,
}).Error
}
func (r *repository) UpdateRedPacketStatus(ctx context.Context, packetID, status string) error {
return r.db.WithContext(ctx).Model(&model.RedPacket{}).
Where("packet_id = ?", packetID).
Update("status", status).Error
}
func (r *repository) UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var rp model.RedPacket
if err := tx.Where("packet_id = ?", packetID).First(&rp).Error; err != nil {
return err
}
totalClaimed := addNumericStrings(rp.ClaimedAmount, claimedAmount)
nextShares := rp.ClaimedShares + 1
updates := map[string]interface{}{
"claimed_amount": totalClaimed,
"claimed_shares": nextShares,
"updated_at": gorm.Expr("CURRENT_TIMESTAMP"),
}
if status != "" {
updates["status"] = status
}
return tx.Model(&model.RedPacket{}).
Where("id = ?", rp.ID).
Updates(updates).Error
})
}
func (r *repository) CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error {
return r.db.WithContext(ctx).Create(auth).Error
}
func (r *repository) GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error) {
var auth model.RedPacketClaimAuth
err := r.db.WithContext(ctx).Where("packet_id = ? AND claimer = ? AND used = false", packetID, claimer).First(&auth).Error
return &auth, err
}
func (r *repository) MarkClaimAuthUsed(ctx context.Context, authNonce string) error {
return r.db.WithContext(ctx).Model(&model.RedPacketClaimAuth{}).
Where("auth_nonce = ?", authNonce).
Update("used", true).Error
}
func (r *repository) GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error) {
var claim model.RedPacketClaim
err := r.db.WithContext(ctx).
Where("packet_id = ? AND claimer_wallet = ?", packetID, claimer).
Order("created_at desc").
First(&claim).Error
return &claim, err
}
func (r *repository) GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error) {
var claim model.RedPacketClaim
err := r.db.WithContext(ctx).
Where("packet_id = ? AND user_id = ?", packetID, userID).
Order("created_at desc").
First(&claim).Error
return &claim, err
}
func (r *repository) SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error {
if claim.UserID != "" {
var existing model.RedPacketClaim
err := r.db.WithContext(ctx).
Where("packet_id = ? AND user_id = ?", claim.PacketID, claim.UserID).
First(&existing).Error
if err == nil {
claim.ID = existing.ID
return r.db.WithContext(ctx).Model(&model.RedPacketClaim{}).
Where("id = ?", existing.ID).
Updates(map[string]interface{}{
"claimer_wallet": existing.ClaimerWallet,
"auth_nonce": claim.AuthNonce,
"claim_tx_hash": claim.ClaimTxHash,
"claimed_amount": claim.ClaimedAmount,
"block_number": claim.BlockNumber,
"status": claim.Status,
"updated_at": claim.UpdatedAt,
}).Error
}
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
}
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "claim_tx_hash"}},
DoUpdates: clause.AssignmentColumns([]string{
"user_id",
"packet_id",
"claimer_wallet",
"auth_nonce",
"claimed_amount",
"block_number",
"status",
"updated_at",
}),
}).Create(claim).Error
}
func (r *repository) GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, error) {
var claims []model.RedPacketClaim
err := r.db.WithContext(ctx).Where("packet_id = ?", packetID).Order("created_at desc").Find(&claims).Error
return claims, err
}
func (r *repository) SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error {
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "tx_hash"}},
DoNothing: true,
}).Create(refund).Error
}
func (r *repository) CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
return r.db.WithContext(ctx).Create(challenge).Error
}
func (r *repository) GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error) {
var challenge model.WalletBindingChallenge
err := r.db.WithContext(ctx).Where("challenge_id = ?", challengeID).First(&challenge).Error
return &challenge, err
}
func (r *repository) UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
return r.db.WithContext(ctx).Model(&model.WalletBindingChallenge{}).
Where("challenge_id = ?", challenge.ChallengeID).
Updates(map[string]interface{}{
"status": challenge.Status,
"signature": challenge.Signature,
"verified_at": challenge.VerifiedAt,
"updated_at": challenge.UpdatedAt,
}).Error
}
func (r *repository) UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error {
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{
{Name: "user_id"},
{Name: "chain_type"},
{Name: "wallet_address"},
},
DoUpdates: clause.AssignmentColumns([]string{
"chain_id",
"status",
"challenge_id",
"verified_at",
"revoked_at",
"updated_at",
}),
}).Create(binding).Error
}
func (r *repository) GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error) {
var binding model.WalletBinding
err := r.db.WithContext(ctx).
Where("user_id = ? AND chain_type = ? AND wallet_address = ? AND status = ?", userID, chainType, walletAddress, "ACTIVE").
First(&binding).Error
return &binding, err
}
func addNumericStrings(current, delta string) string {
left := new(big.Int)
if current != "" {
left.SetString(current, 10)
}
right := new(big.Int)
if delta != "" {
right.SetString(delta, 10)
}
return new(big.Int).Add(left, right).String()
}

@ -1,138 +0,0 @@
package service
import (
"context"
"fmt"
"math/big"
"redpacket/internal/chain"
"github.com/ethereum/go-ethereum/common"
)
// AdminService handles administrative operations on the RedPacket contract
type AdminService struct {
ethClient *chain.ChainClient
tronClient *chain.TronClient
}
func NewAdminService(ethClient *chain.ChainClient, tronClient *chain.TronClient) *AdminService {
return &AdminService{
ethClient: ethClient,
tronClient: tronClient,
}
}
func (s *AdminService) SetSigner(ctx context.Context, signerAddress string) error {
if s.ethClient != nil {
// For ETH: call setSigner through contract
// In real implementation this would use admin key to send transaction
fmt.Printf("ETH: Setting signer to %s (mock)\n", signerAddress)
return nil
}
if s.tronClient != nil {
_, err := s.tronClient.SendAdminTransaction(ctx, "setSigner", signerAddress)
return err
}
return fmt.Errorf("no blockchain client configured")
}
func (s *AdminService) SetToken(ctx context.Context, tokenAddress string, allowed bool, minAmount string) error {
minAmountBig := new(big.Int)
if minAmount != "" {
minAmountBig.SetString(minAmount, 10)
} else {
minAmountBig.SetInt64(0)
}
if s.ethClient != nil {
fmt.Printf("ETH: Setting token %s allowed=%v minAmount=%s (mock)\n", tokenAddress, allowed, minAmount)
return nil
}
if s.tronClient != nil {
_, err := s.tronClient.SendAdminTransaction(ctx, "setAllowedToken", tokenAddress, allowed, minAmountBig)
return err
}
return fmt.Errorf("no blockchain client configured")
}
func (s *AdminService) SetExpiry(ctx context.Context, expirySeconds int64) error {
if s.ethClient != nil {
fmt.Printf("ETH: Setting default expiry to %d seconds (mock)\n", expirySeconds)
return nil
}
if s.tronClient != nil {
_, err := s.tronClient.SendAdminTransaction(ctx, "setDefaultExpiryDuration", expirySeconds)
return err
}
return fmt.Errorf("no blockchain client configured")
}
func (s *AdminService) SetAllowAllTokens(ctx context.Context, allowAll bool) error {
if s.ethClient != nil {
fmt.Printf("ETH: Setting allowAllTokens=%v (mock)\n", allowAll)
return nil
}
if s.tronClient != nil {
_, err := s.tronClient.SendAdminTransaction(ctx, "setAllowAllTokens", allowAll)
return err
}
return fmt.Errorf("no blockchain client configured")
}
func (s *AdminService) SetNativeTokenEnabled(ctx context.Context, enabled bool) error {
if s.ethClient != nil {
fmt.Printf("ETH: Setting native token enabled=%v (mock)\n", enabled)
return nil
}
if s.tronClient != nil {
_, err := s.tronClient.SendAdminTransaction(ctx, "setNativeTokenEnabled", enabled)
return err
}
return fmt.Errorf("no blockchain client configured")
}
func (s *AdminService) ParseTxEvents(ctx context.Context, txHash, chain string) (map[string]interface{}, error) {
if chain == "tron" && s.tronClient != nil {
return map[string]interface{}{
"chain": "tron",
"tx_hash": txHash,
"status": "parsed",
"note": "TRON event parsing not fully implemented in this version",
}, nil
}
if s.ethClient != nil {
txHashBytes := common.HexToHash(txHash)
events, err := s.ethClient.ParseTransactionReceipt(ctx, txHashBytes)
if err != nil {
return nil, err
}
eventList := make([]map[string]interface{}, len(events))
for i, e := range events {
eventList[i] = map[string]interface{}{
"name": e.Name,
"data": e.Data,
}
}
return map[string]interface{}{
"chain": "eth",
"tx_hash": txHash,
"events": eventList,
}, nil
}
return nil, fmt.Errorf("no client available for chain: %s", chain)
}

@ -1,386 +0,0 @@
package service
import (
"context"
"encoding/json"
"testing"
"time"
"redpacket/internal/authctx"
"redpacket/internal/model"
"redpacket/internal/repository"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func newTestService(t *testing.T) (*RedPacketService, repository.Repository) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("gorm.Open() error = %v", err)
}
if err := db.AutoMigrate(
&model.RedPacket{},
&model.RedPacketClaim{},
&model.RedPacketClaimAuth{},
&model.RedPacketRefund{},
&model.WalletBindingChallenge{},
&model.WalletBinding{},
); err != nil {
t.Fatalf("AutoMigrate() error = %v", err)
}
repo := repository.New(db)
svc := NewRedPacketService(repo, nil, nil, "")
return svc, repo
}
func seedWalletBinding(t *testing.T, repo repository.Repository, userID, chainType, wallet string) {
t.Helper()
err := repo.UpsertWalletBinding(context.Background(), &model.WalletBinding{
UserID: userID,
ChainType: chainType,
WalletAddress: wallet,
Status: "ACTIVE",
ChallengeID: "test-challenge",
VerifiedAt: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
if err != nil {
t.Fatalf("UpsertWalletBinding() error = %v", err)
}
}
func TestCanClaimRejectsExpiredAndAlreadyClaimed(t *testing.T) {
svc, repo := newTestService(t)
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
activePacket := &model.RedPacket{
BizID: "biz-active",
ChainType: "EVM",
PacketID: "1001",
CreatorUserID: "u1",
CreatorWallet: "0xabc",
GroupID: "g-active",
Status: "ACTIVE",
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.CreateRedPacket(ctx, activePacket); err != nil {
t.Fatalf("CreateRedPacket(active) error = %v", err)
}
seedWalletBinding(t, repo, "u2", "EVM", "0xclaimer")
claim := &model.RedPacketClaim{
PacketID: "1001",
ClaimerWallet: "0xclaimer",
ClaimTxHash: "0xtx1",
Status: "CONFIRMED",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.SaveClaim(ctx, claim); err != nil {
t.Fatalf("SaveClaim() error = %v", err)
}
if err := svc.CanClaim(ctx, "1001", "0xclaimer", "u2"); err == nil || err.Error() != "already claimed" {
t.Fatalf("expected already claimed error, got %v", err)
}
expiredPacket := &model.RedPacket{
BizID: "biz-expired",
ChainType: "EVM",
PacketID: "1002",
CreatorUserID: "u1",
CreatorWallet: "0xabc",
GroupID: "g-expired",
Status: "ACTIVE",
ExpiryAt: time.Now().Add(-1 * time.Minute).Unix(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.CreateRedPacket(ctx, expiredPacket); err != nil {
t.Fatalf("CreateRedPacket(expired) error = %v", err)
}
seedWalletBinding(t, repo, "u3", "EVM", "0xfresh")
if err := svc.CanClaim(authctx.WithCurrentUserID(context.Background(), "u3"), "1002", "0xfresh", "u3"); err == nil || err.Error() != "packet is expired" {
t.Fatalf("expected expired error, got %v", err)
}
}
func TestCanClaimRejectsAlreadyClaimedByUserID(t *testing.T) {
svc, repo := newTestService(t)
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
packet := &model.RedPacket{
BizID: "biz-user-claimed",
ChainType: "EVM",
PacketID: "1003",
CreatorUserID: "u1",
CreatorWallet: "0xabc",
GroupID: "g-user-claimed",
Status: "ACTIVE",
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.CreateRedPacket(ctx, packet); err != nil {
t.Fatalf("CreateRedPacket() error = %v", err)
}
seedWalletBinding(t, repo, "u2", "EVM", "0xanother-wallet")
claim := &model.RedPacketClaim{
PacketID: "1003",
UserID: "u2",
ClaimerWallet: "0xclaimer",
ClaimTxHash: "0xtx-user-claimed",
Status: "CONFIRMED",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.SaveClaim(ctx, claim); err != nil {
t.Fatalf("SaveClaim() error = %v", err)
}
if err := svc.CanClaim(ctx, "1003", "0xanother-wallet", "u2"); err == nil || err.Error() != "user already claimed" {
t.Fatalf("expected user already claimed error, got %v", err)
}
}
func TestCanClaimUsesPacketTypeRules(t *testing.T) {
svc, repo := newTestService(t)
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
groupPacket := &model.RedPacket{
BizID: "biz-group",
ChainType: "EVM",
PacketID: "1101",
CreatorUserID: "u1",
CreatorWallet: "0xabc",
PacketType: 0,
Status: "ACTIVE",
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.CreateRedPacket(ctx, groupPacket); err != nil {
t.Fatalf("CreateRedPacket(group) error = %v", err)
}
seedWalletBinding(t, repo, "u2", "EVM", "0xclaimer")
if err := svc.CanClaim(ctx, "1101", "0xclaimer", "u2"); err == nil || err.Error() != "group_id is required for fixed packet claim" {
t.Fatalf("expected missing group_id error, got %v", err)
}
transferPacket := &model.RedPacket{
BizID: "biz-transfer",
ChainType: "EVM",
PacketID: "1102",
CreatorUserID: "u1",
CreatorWallet: "0xabc",
PacketType: 2,
ReceiverUserID: "u-receiver",
Status: "ACTIVE",
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.CreateRedPacket(ctx, transferPacket); err != nil {
t.Fatalf("CreateRedPacket(transfer) error = %v", err)
}
seedWalletBinding(t, repo, "u-other", "EVM", "0xclaimer")
if err := svc.CanClaim(ctx, "1102", "0xclaimer", "u-other"); err == nil || err.Error() != "user is not the designated receiver" {
t.Fatalf("expected designated receiver error, got %v", err)
}
}
func TestCreateOrderPersistsScopeFields(t *testing.T) {
svc, repo := newTestService(t)
ctx := authctx.WithCurrentUserID(context.Background(), "u-create")
result, err := svc.CreateOrder(ctx, &CreateOrderRequest{
ChainType: "EVM",
CreatorWallet: "0x1111111111111111111111111111111111111111",
GroupID: "g-100",
ScopeType: "group",
PacketType: 1,
Token: "0x2222222222222222222222222222222222222222",
TotalAmount: "1000",
TotalShares: 10,
ReceiverUserIDs: []string{"u2", "u3"},
})
if err != nil {
t.Fatalf("CreateOrder() error = %v", err)
}
bizID, _ := result["biz_id"].(string)
record, err := repo.GetRedPacketByBizID(ctx, bizID)
if err != nil {
t.Fatalf("GetRedPacketByBizID() error = %v", err)
}
if record.ScopeType != "GROUP" {
t.Fatalf("scope type mismatch: got %s", record.ScopeType)
}
if record.ChainType != "EVM" {
t.Fatalf("chain type mismatch: got %s", record.ChainType)
}
if record.GroupID != "g-100" {
t.Fatalf("group id mismatch: got %s", record.GroupID)
}
var got []string
if err := json.Unmarshal([]byte(record.ReceiverUserIDs), &got); err != nil {
t.Fatalf("Unmarshal(receiver_user_ids) error = %v", err)
}
if len(got) != 2 || got[0] != "u2" || got[1] != "u3" {
t.Fatalf("receiver_user_ids mismatch: got %+v", got)
}
}
func TestCreatedCallbackUpdatesBindingAndScope(t *testing.T) {
svc, repo := newTestService(t)
ctx := authctx.WithCurrentUserID(context.Background(), "u-create")
result, err := svc.CreateOrder(ctx, &CreateOrderRequest{
ChainType: "TRON",
CreatorWallet: "0x1111111111111111111111111111111111111111",
PacketType: 2,
Token: "0x0000000000000000000000000000000000000000",
TotalAmount: "1000",
TotalShares: 1,
})
if err != nil {
t.Fatalf("CreateOrder() error = %v", err)
}
bizID, _ := result["biz_id"].(string)
err = svc.CreatedCallback(ctx, &CreatedCallbackRequest{
BizID: bizID,
TxHash: "0xabc123",
PacketID: "3001",
ScopeType: "DIRECT",
ReceiverUserID: "u-receiver",
})
if err != nil {
t.Fatalf("CreatedCallback() error = %v", err)
}
record, err := repo.GetRedPacketByBizID(ctx, bizID)
if err != nil {
t.Fatalf("GetRedPacketByBizID() error = %v", err)
}
if record.PacketID != "3001" {
t.Fatalf("packet id mismatch: got %s", record.PacketID)
}
if record.ChainType != "TRON" {
t.Fatalf("chain type mismatch: got %s", record.ChainType)
}
if record.TxHash != "0xabc123" {
t.Fatalf("tx hash mismatch: got %s", record.TxHash)
}
if record.Status != "ACTIVE" {
t.Fatalf("status mismatch: got %s", record.Status)
}
if record.ScopeType != "DIRECT" {
t.Fatalf("scope type mismatch: got %s", record.ScopeType)
}
if record.ReceiverUserID != "u-receiver" {
t.Fatalf("receiver user mismatch: got %s", record.ReceiverUserID)
}
}
func TestIssueClaimSignValidatesInputsAndPersistsAuth(t *testing.T) {
svc, repo := newTestService(t)
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
packet := &model.RedPacket{
BizID: "biz-sign",
ChainType: "EVM",
PacketID: "2001",
CreatorUserID: "u1",
CreatorWallet: "0xabc",
GroupID: "g-sign",
Status: "ACTIVE",
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.CreateRedPacket(ctx, packet); err != nil {
t.Fatalf("CreateRedPacket() error = %v", err)
}
seedWalletBinding(t, repo, "u2", "EVM", "0x1111111111111111111111111111111111111111")
if _, err := svc.IssueClaimSign(ctx, "bad-packet-id", "0x1111111111111111111111111111111111111111", "0"); err == nil {
t.Fatalf("expected invalid packet id error")
}
result, err := svc.IssueClaimSign(ctx, "2001", "0x1111111111111111111111111111111111111111", "123")
if err != nil {
t.Fatalf("IssueClaimSign() error = %v", err)
}
auth, err := repo.GetClaimAuth(ctx, "2001", "0x1111111111111111111111111111111111111111")
if err != nil {
t.Fatalf("GetClaimAuth() error = %v", err)
}
if auth.AuthNonce == "" {
t.Fatalf("expected auth nonce to be persisted")
}
if auth.RandomSeed != "123" {
t.Fatalf("random seed mismatch: got %s", auth.RandomSeed)
}
if result["auth_nonce"] == "" {
t.Fatalf("expected auth_nonce in response")
}
}
func TestClaimResultPersistsPendingWithoutChainParser(t *testing.T) {
svc, repo := newTestService(t)
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
packet := &model.RedPacket{
BizID: "biz-claim-result",
ChainType: "EVM",
PacketID: "2101",
CreatorUserID: "u1",
CreatorWallet: "0xabc",
GroupID: "g-1",
PacketType: 0,
Status: "ACTIVE",
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := repo.CreateRedPacket(ctx, packet); err != nil {
t.Fatalf("CreateRedPacket() error = %v", err)
}
seedWalletBinding(t, repo, "u2", "EVM", "0x1111111111111111111111111111111111111111")
err := svc.ClaimResult(ctx, &ClaimResultRequest{
PacketID: "2101",
Claimer: "0x1111111111111111111111111111111111111111",
TxHash: "0xtx-claim",
})
if err != nil {
t.Fatalf("ClaimResult() error = %v", err)
}
claim, err := repo.GetClaimByPacketIDAndClaimer(ctx, "2101", "0x1111111111111111111111111111111111111111")
if err != nil {
t.Fatalf("GetClaimByPacketIDAndClaimer() error = %v", err)
}
if claim.Status != "PENDING" {
t.Fatalf("claim status mismatch: got %s", claim.Status)
}
if claim.UserID != "u2" {
t.Fatalf("user id mismatch: got %s", claim.UserID)
}
}

@ -1,163 +1,12 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"redpacket/config"
"redpacket/internal/chain"
"redpacket/internal/handler"
"redpacket/internal/model"
"redpacket/internal/repository"
"redpacket/internal/service"
"redpacket/router"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/openimsdk/open-im-server/v3/pkg/common/cmd"
"github.com/openimsdk/tools/system/program"
)
func main() {
// Load configuration
cfgFile := ""
if len(os.Args) > 1 {
cfgFile = os.Args[1]
}
config.Load(cfgFile)
cfg := &config.Cfg
// Connect to database
db, err := openDB(cfg)
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
// Auto-migrate models
if err := db.AutoMigrate(
&model.RedPacket{},
&model.RedPacketClaim{},
&model.RedPacketClaimAuth{},
&model.RedPacketRefund{},
&model.WalletBindingChallenge{},
&model.WalletBinding{},
); err != nil {
log.Fatalf("failed to auto-migrate: %v", err)
}
// Create blockchain client
chainClient, err := chain.NewClient(
cfg.Chain.RPCURL,
cfg.Chain.ContractAddress,
cfg.Chain.ChainID,
cfg.Chain.SignerPrivateKey,
cfg.Chain.ConfigAdminPrivateKey,
)
if err != nil {
log.Printf("Warning: failed to create chain client: %v (continuing with mock mode)", err)
// Continue without blockchain for now - can be configured later
}
// Create TRON client if configured
var tronClient *chain.TronClient
if cfg.Tron.FullNodeURL != "" {
abiJSON, err := chain.ExtractABIFromEmbeddedArtifact()
if err != nil {
log.Printf("Warning: failed to load ABI for TRON: %v", err)
} else {
tronClient, err = chain.NewTronClient(
cfg.Tron.FullNodeURL,
cfg.Tron.ContractBase58,
cfg.Tron.OwnerBase58,
cfg.Tron.PrivateKeyHex,
abiJSON,
cfg.Tron.FeeLimit,
)
if err != nil {
log.Printf("Warning: failed to create TRON client: %v", err)
tronClient = nil
} else {
log.Println("✅ TRON client initialized successfully")
}
}
}
// Create repository and service
repo := repository.New(db)
rpSvc := service.NewRedPacketService(repo, chainClient, tronClient, cfg.Chain.SignerPrivateKey)
// Create admin service and handler
adminSvc := service.NewAdminService(chainClient, tronClient)
adminHandler := handler.NewAdminHandler(adminSvc)
// Create user handler
rpHandler := handler.NewRedPacketHandler(rpSvc)
// Setup router
r := gin.Default()
router.Setup(r, rpHandler, adminHandler)
// Start blockchain indexers
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// ETH Indexer
if chainClient != nil {
ethIndexer := chain.NewIndexer(chainClient, repo, cfg.Indexer.PollInterval, 0)
ethIndexer.Start(ctx)
log.Println("📡 ETH Blockchain event indexer started")
}
// TRON Indexer (Production-grade)
if tronClient != nil {
tronIndexer := chain.NewTronIndexer(tronClient, repo, cfg.Indexer.PollInterval, 0)
tronIndexer.Start(ctx)
log.Println("📡 TRON Blockchain event indexer started (Production mode)")
}
// Start HTTP server with graceful shutdown
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
Handler: r,
}
go func() {
log.Printf("🚀 RedPacket service listening on :%d", cfg.Server.Port)
log.Printf("📋 Health check: http://localhost:%d/health", cfg.Server.Port)
log.Printf("📋 API docs: see backend-api.md")
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down server...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("server forced shutdown: %v", err)
}
log.Println("server stopped")
}
func openDB(cfg *config.Config) (*gorm.DB, error) {
switch cfg.DB.Driver {
case "mysql":
return gorm.Open(mysql.Open(cfg.DB.DSN), &gorm.Config{})
case "sqlite", "":
return gorm.Open(sqlite.Open(cfg.DB.DSN), &gorm.Config{})
default:
return nil, fmt.Errorf("unsupported db.driver: %s", cfg.DB.Driver)
if err := cmd.NewRedPacketRpcCmd().Exec(); err != nil {
program.ExitWithError(err)
}
}

@ -1,40 +0,0 @@
package resp
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func OK(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "ok",
Data: data,
})
}
func Fail(c *gin.Context, httpCode, code int, message string) {
c.JSON(httpCode, Response{
Code: code,
Message: message,
})
}
func BadRequest(c *gin.Context, message string) {
Fail(c, http.StatusBadRequest, 400, message)
}
func Forbidden(c *gin.Context, message string) {
Fail(c, http.StatusForbidden, 403, message)
}
func InternalError(c *gin.Context, message string) {
Fail(c, http.StatusInternalServerError, 500, message)
}

@ -1,37 +0,0 @@
package router
import (
"redpacket/internal/handler"
"github.com/gin-gonic/gin"
)
func Setup(r *gin.Engine, rpHandler *handler.RedPacketHandler, adminHandler *handler.AdminHandler) {
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// User-facing red packet APIs
api := r.Group("/api/redpacket")
{
api.POST("/create-order", rpHandler.CreateOrder)
api.POST("/created-callback", rpHandler.CreatedCallback)
api.GET("/detail", rpHandler.Detail)
api.POST("/claim-sign", rpHandler.ClaimSign)
api.POST("/claim-result", rpHandler.ClaimResult)
api.POST("/wallet-bind/challenge", rpHandler.WalletBindChallenge)
api.POST("/wallet-bind/confirm", rpHandler.WalletBindConfirm)
api.GET("/wallet-bind/detail", rpHandler.WalletBindDetail)
}
// Admin APIs - should be protected with authentication in production
admin := r.Group("/admin/redpacket")
{
admin.POST("/set-signer", adminHandler.SetSigner)
admin.POST("/set-token", adminHandler.SetToken)
admin.POST("/set-expiry", adminHandler.SetExpiry)
admin.POST("/set-allow-all-tokens", adminHandler.SetAllowAllTokens)
admin.POST("/set-native-token", adminHandler.SetNativeTokenEnabled)
admin.POST("/parse-tx-events", adminHandler.ParseTxEvents)
}
}

@ -0,0 +1,31 @@
rpc:
registerIP: ""
listenIP: 0.0.0.0
autoSetPorts: false
ports: [10560]
prometheus:
enable: false
ports: [12560]
# EVM (Ethereum / Polygon / BSC / ...) chain configuration.
# Leave rpcURL empty to disable the EVM client; the RPC service will then
# only expose TRON-related functionality (or the offchain code paths).
chain:
rpcURL: ""
contractAddress: ""
chainID: 0
signerPrivateKey: ""
configAdminPrivateKey: ""
# TRON full-node configuration. Leave fullNodeURL empty to disable TRON.
tron:
fullNodeURL: ""
contractBase58: ""
ownerBase58: ""
privateKeyHex: ""
feeLimit: 100000000
# Indexer polling interval (in seconds). Used by both EVM and TRON event indexers.
indexer:
pollInterval: 5

@ -12,6 +12,7 @@ rpcRegisterName:
captcha: captcha
rtc: rtc
crypto: crypto
redPacket: redPacket
imAdminUserID: [ imAdmin ]

@ -32,7 +32,9 @@ require github.com/google/uuid v1.6.0
require (
github.com/IBM/sarama v1.43.0
github.com/fatih/color v1.14.1
github.com/VirgilSecurity/virgil-sdk-go v5.2.1+incompatible
github.com/ethereum/go-ethereum v1.14.12
github.com/fatih/color v1.16.0
github.com/gin-contrib/gzip v1.0.1
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-redis/redismock/v9 v9.2.0
@ -64,8 +66,7 @@ require (
cloud.google.com/go/longrunning v0.5.5 // indirect
cloud.google.com/go/storage v1.40.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/VirgilSecurity/virgil-crypto-go v0.0.0-20180221191626-33caf95f9a5d // indirect
github.com/VirgilSecurity/virgil-sdk-go v5.2.1+incompatible // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.32.5 // indirect
@ -89,6 +90,7 @@ require (
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
@ -96,9 +98,15 @@ require (
github.com/clbanning/mxj v1.8.4 // 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/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/dennwc/iters v1.2.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@ -107,6 +115,8 @@ require (
github.com/eapache/queue v1.1.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // 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/felixge/httpsnoop v1.0.4 // indirect
github.com/frostbyte73/core v0.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@ -126,7 +136,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/google/cel-go v0.27.0 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.7.0 // indirect
@ -139,6 +149,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
@ -168,6 +179,7 @@ require (
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.69 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@ -214,6 +226,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/supranational/blst v0.3.13 // indirect
github.com/tencentyun/cos-go-sdk-v5 v0.7.47 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
@ -264,6 +277,7 @@ require (
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect

@ -26,6 +26,8 @@ firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaB
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/IBM/sarama v1.43.0 h1:YFFDn8mMI2QL0wOrG0J2sFoVIAFl7hS9JQi2YZsXtJc=
github.com/IBM/sarama v1.43.0/go.mod h1:zlE6HEbC/SMQ9mhEYaF7nNLYOUyrs0obySKCckWP9BM=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
@ -35,8 +37,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
github.com/VirgilSecurity/virgil-crypto-go v0.0.0-20180221191626-33caf95f9a5d h1:ElVLTQRuo+LvdhsvybRwBTXvDCjMyB0Dv4mhOPnjQUQ=
github.com/VirgilSecurity/virgil-crypto-go v0.0.0-20180221191626-33caf95f9a5d/go.mod h1:zyDDPi7Ihhd5JdTYQCcdmzACnF824PYV6E6UELQiZ1w=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
github.com/VirgilSecurity/virgil-sdk-go v5.2.1+incompatible h1:icWPcnsM0eqDs3pNxglM/3FbuF0Y9WUygpRM4PdBbec=
github.com/VirgilSecurity/virgil-sdk-go v5.2.1+incompatible/go.mod h1:8kxwYsqg97YNwiVCrte1fqbP6H9VJ2vjSuyj1p1CP/8=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
@ -85,6 +87,8 @@ 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/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
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/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
@ -112,6 +116,22 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
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/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@ -122,12 +142,23 @@ github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmf
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/dennwc/iters v1.2.2 h1:XH2/Etihiy9ZvPOVCR+icQXeYlhbvS7k0qro4x/2qQo=
github.com/dennwc/iters v1.2.2/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@ -164,8 +195,14 @@ github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9O
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@ -182,6 +219,8 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ=
github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g=
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/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@ -232,6 +271,8 @@ github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
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.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@ -256,8 +297,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
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.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
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/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
@ -286,6 +327,7 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -303,6 +345,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
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/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@ -312,8 +356,18 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
@ -364,6 +418,8 @@ 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.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
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=
@ -402,6 +458,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
@ -411,6 +469,11 @@ github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
@ -441,6 +504,8 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
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/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
@ -526,6 +591,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
@ -534,8 +601,11 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
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/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
@ -581,6 +651,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
github.com/tencentyun/cos-go-sdk-v5 v0.7.47 h1:uoS4Sob16qEYoapkqJq1D1Vnsy9ira9BfNUMtoFYTI4=
@ -595,6 +669,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
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/wenlng/go-captcha/v2 v2.0.5 h1:+1FpVwJZmLCqEHxOt+HvpUArFGo107nRxOeRVHkZhTc=
github.com/wenlng/go-captcha/v2 v2.0.5/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
@ -613,6 +689,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
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=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -825,6 +903,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/virgil.v5 v5.2.1 h1:8NnvRXg66qC6C4uqVhuMEfm8wInUGC+QG2vdbMaCbUI=
@ -855,6 +935,8 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
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=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=

@ -41,7 +41,7 @@ func (c *CaptchaApi) VerifyCaptcha(ctx *gin.Context) {
}
resp, err := c.Client.VerifyCaptcha(ctx, req)
if err != nil {
log.ZError(ctx, "captcha verify rpc failed", err, "captchaID", req.GetCaptchaID(), "x", req.GetX(), "y", req.GetY())
log.ZError(ctx, "captcha verify rpc failed", err, "captchaID", req.GetCaptchaID(), "clickPoints", req.GetClickPoints())
apiresp.GinError(ctx, err)
return
}

@ -48,6 +48,7 @@ func Start(ctx context.Context, index int, cfg *Config) error {
client, err = kdisc.NewDiscoveryRegister(&cfg.Discovery, &cfg.Share, []string{
cfg.Share.RpcRegisterName.MessageGateway,
cfg.Share.RpcRegisterName.Captcha,
cfg.Share.RpcRegisterName.RedPacket,
})
if err != nil {
return errs.WrapMsg(err, "failed to register discovery service")

@ -0,0 +1,217 @@
package api
import (
"github.com/gin-gonic/gin"
pbredpacket "github.com/openimsdk/protocol/redpacket"
"github.com/openimsdk/tools/a2r"
"github.com/openimsdk/tools/apiresp"
"github.com/openimsdk/tools/log"
)
type RedPacketApi struct {
Client pbredpacket.RedPacketClient
}
func NewRedPacketApi(client pbredpacket.RedPacketClient) *RedPacketApi {
return &RedPacketApi{Client: client}
}
func (h *RedPacketApi) CreateOrder(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.CreateOrderReq](ctx)
if err != nil {
log.ZError(ctx, "redpacket create order parse failed", err)
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.CreateOrder(ctx, req)
if err != nil {
log.ZError(ctx, "redpacket create order rpc failed", err)
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) CreatedCallback(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.CreatedCallbackReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.CreatedCallback(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) GetDetail(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.GetDetailReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.GetDetail(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) IssueClaimSign(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.IssueClaimSignReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.IssueClaimSign(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) ClaimResult(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.ClaimResultReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.ClaimResult(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) IssueWalletBindChallenge(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.IssueWalletBindChallengeReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.IssueWalletBindChallenge(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) ConfirmWalletBind(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.ConfirmWalletBindReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.ConfirmWalletBind(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) GetWalletBinding(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.GetWalletBindingReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.GetWalletBinding(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
// Admin endpoints
func (h *RedPacketApi) AdminSetSigner(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetSignerReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.SetSigner(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) AdminSetToken(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetTokenReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.SetToken(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) AdminSetExpiry(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetExpiryReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.SetExpiry(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) AdminSetAllowAllTokens(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetAllowAllTokensReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.SetAllowAllTokens(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) AdminSetNativeTokenEnabled(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.SetNativeTokenEnabledReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.SetNativeTokenEnabled(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}
func (h *RedPacketApi) AdminParseTxEvents(ctx *gin.Context) {
req, err := a2r.ParseRequestNotCheck[pbredpacket.ParseTxEventsReq](ctx)
if err != nil {
apiresp.GinError(ctx, err)
return
}
resp, err := h.Client.ParseTxEvents(ctx, req)
if err != nil {
apiresp.GinError(ctx, err)
return
}
apiresp.GinSuccess(ctx, resp)
}

@ -13,6 +13,7 @@ import (
"github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/relation"
pbcrypto "github.com/openimsdk/protocol/crypto"
pbredpacket "github.com/openimsdk/protocol/redpacket"
"github.com/openimsdk/protocol/rtc"
"github.com/openimsdk/protocol/third"
"github.com/openimsdk/protocol/user"
@ -117,6 +118,10 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
if err != nil {
return nil, err
}
redpacketConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.RedPacket)
if err != nil {
return nil, err
}
gin.SetMode(gin.ReleaseMode)
r := gin.New()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
@ -363,6 +368,28 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
cryptoGroup.POST("/integrity_report", cr.IntegrityReport)
}
// RedPacket
{
rp := NewRedPacketApi(pbredpacket.NewRedPacketClient(redpacketConn))
redpacketGroup := r.Group("/redpacket")
redpacketGroup.POST("/create_order", rp.CreateOrder)
redpacketGroup.POST("/created_callback", rp.CreatedCallback)
redpacketGroup.POST("/detail", rp.GetDetail)
redpacketGroup.POST("/issue_claim_sign", rp.IssueClaimSign)
redpacketGroup.POST("/claim_result", rp.ClaimResult)
redpacketGroup.POST("/wallet_bind/challenge", rp.IssueWalletBindChallenge)
redpacketGroup.POST("/wallet_bind/confirm", rp.ConfirmWalletBind)
redpacketGroup.POST("/wallet_bind/detail", rp.GetWalletBinding)
adminGroup := redpacketGroup.Group("/admin")
adminGroup.POST("/set_signer", rp.AdminSetSigner)
adminGroup.POST("/set_token", rp.AdminSetToken)
adminGroup.POST("/set_expiry", rp.AdminSetExpiry)
adminGroup.POST("/set_allow_all_tokens", rp.AdminSetAllowAllTokens)
adminGroup.POST("/set_native_token_enabled", rp.AdminSetNativeTokenEnabled)
adminGroup.POST("/parse_tx_events", rp.AdminParseTxEvents)
}
{
statisticsGroup := r.Group("/statistics")
statisticsGroup.POST("/user/register", u.UserRegisterCount)

@ -120,11 +120,11 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc
log.ZError(ctx, "captcha insert mongodb failed", err, "captchaID", id)
return nil, err
}
_ = tileImage
return &pbcaptcha.GenerateCaptchaResp{
CaptchaID: id,
MasterImage: masterImage,
TileImage: tileImage,
TileY: int32(block.DY),
ThumbImage: tileImage,
ExpireAt: expiredAt.Unix(),
}, nil
}
@ -159,9 +159,14 @@ func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptcha
log.ZWarn(ctx, "captcha expired", nil, "captchaID", req.CaptchaID, "expiredAt", doc.ExpiredAt.Unix())
return nil, servererrs.ErrFileUploadedExpired.WrapMsg("captcha expired", "captchaID", req.CaptchaID)
}
success := slide.Validate(int(req.X), int(req.Y), doc.X, doc.Y, s.conf.VerifyPadding)
var x, y int32
if pts := req.GetClickPoints(); len(pts) > 0 && pts[0] != nil {
x = pts[0].GetX()
y = pts[0].GetY()
}
success := slide.Validate(int(x), int(y), doc.X, doc.Y, s.conf.VerifyPadding)
if !success {
log.ZError(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", req.X, "y", req.Y, "docX", doc.X, "docY", doc.Y)
log.ZError(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", x, "y", y, "docX", doc.X, "docY", doc.Y)
}
return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil
}

@ -0,0 +1,142 @@
package redpacket
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
pbredpacket "github.com/openimsdk/protocol/redpacket"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
)
func (s *redPacketServer) SetSigner(ctx context.Context, req *pbredpacket.SetSignerReq) (*pbredpacket.SetSignerResp, error) {
if req.SignerAddress == "" {
return nil, errs.ErrArgs.WrapMsg("signer_address is required")
}
if s.chainClient != nil {
log.ZInfo(ctx, "redpacket admin setSigner (eth mock)", "signerAddress", req.SignerAddress)
return &pbredpacket.SetSignerResp{Message: "signer address updated successfully"}, nil
}
if s.tronClient != nil {
if _, err := s.tronClient.SendAdminTransaction(ctx, "setSigner", req.SignerAddress); err != nil {
return nil, errs.ErrInternalServer.WrapMsg("setSigner failed: " + err.Error())
}
return &pbredpacket.SetSignerResp{Message: "signer address updated successfully"}, nil
}
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
}
func (s *redPacketServer) SetToken(ctx context.Context, req *pbredpacket.SetTokenReq) (*pbredpacket.SetTokenResp, error) {
if req.TokenAddress == "" {
return nil, errs.ErrArgs.WrapMsg("token_address is required")
}
minAmountBig := new(big.Int)
if req.MinAmount != "" {
minAmountBig.SetString(req.MinAmount, 10)
}
if s.chainClient != nil {
log.ZInfo(ctx, "redpacket admin setToken (eth mock)",
"tokenAddress", req.TokenAddress,
"allowed", req.Allowed,
"minAmount", req.MinAmount,
)
return &pbredpacket.SetTokenResp{Message: "token configuration updated"}, nil
}
if s.tronClient != nil {
if _, err := s.tronClient.SendAdminTransaction(ctx, "setAllowedToken", req.TokenAddress, req.Allowed, minAmountBig); err != nil {
return nil, errs.ErrInternalServer.WrapMsg("setAllowedToken failed: " + err.Error())
}
return &pbredpacket.SetTokenResp{Message: "token configuration updated"}, nil
}
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
}
func (s *redPacketServer) SetExpiry(ctx context.Context, req *pbredpacket.SetExpiryReq) (*pbredpacket.SetExpiryResp, error) {
if req.ExpirySeconds <= 0 {
return nil, errs.ErrArgs.WrapMsg("expiry_seconds must be positive")
}
if s.chainClient != nil {
log.ZInfo(ctx, "redpacket admin setExpiry (eth mock)", "expirySeconds", req.ExpirySeconds)
return &pbredpacket.SetExpiryResp{Message: "expiry duration updated"}, nil
}
if s.tronClient != nil {
if _, err := s.tronClient.SendAdminTransaction(ctx, "setDefaultExpiryDuration", req.ExpirySeconds); err != nil {
return nil, errs.ErrInternalServer.WrapMsg("setDefaultExpiryDuration failed: " + err.Error())
}
return &pbredpacket.SetExpiryResp{Message: "expiry duration updated"}, nil
}
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
}
func (s *redPacketServer) SetAllowAllTokens(ctx context.Context, req *pbredpacket.SetAllowAllTokensReq) (*pbredpacket.SetAllowAllTokensResp, error) {
if s.chainClient != nil {
log.ZInfo(ctx, "redpacket admin setAllowAllTokens (eth mock)", "allowAll", req.AllowAll)
return &pbredpacket.SetAllowAllTokensResp{Message: "allow all tokens setting updated"}, nil
}
if s.tronClient != nil {
if _, err := s.tronClient.SendAdminTransaction(ctx, "setAllowAllTokens", req.AllowAll); err != nil {
return nil, errs.ErrInternalServer.WrapMsg("setAllowAllTokens failed: " + err.Error())
}
return &pbredpacket.SetAllowAllTokensResp{Message: "allow all tokens setting updated"}, nil
}
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
}
func (s *redPacketServer) SetNativeTokenEnabled(ctx context.Context, req *pbredpacket.SetNativeTokenEnabledReq) (*pbredpacket.SetNativeTokenEnabledResp, error) {
if s.chainClient != nil {
log.ZInfo(ctx, "redpacket admin setNativeTokenEnabled (eth mock)", "enabled", req.Enabled)
return &pbredpacket.SetNativeTokenEnabledResp{Message: "native token setting updated"}, nil
}
if s.tronClient != nil {
if _, err := s.tronClient.SendAdminTransaction(ctx, "setNativeTokenEnabled", req.Enabled); err != nil {
return nil, errs.ErrInternalServer.WrapMsg("setNativeTokenEnabled failed: " + err.Error())
}
return &pbredpacket.SetNativeTokenEnabledResp{Message: "native token setting updated"}, nil
}
return nil, errs.ErrInternalServer.WrapMsg("no blockchain client configured")
}
func (s *redPacketServer) ParseTxEvents(ctx context.Context, req *pbredpacket.ParseTxEventsReq) (*pbredpacket.ParseTxEventsResp, error) {
if req.TxHash == "" {
return nil, errs.ErrArgs.WrapMsg("tx_hash is required")
}
if req.Chain == "tron" && s.tronClient != nil {
return &pbredpacket.ParseTxEventsResp{
Chain: "tron",
TxHash: req.TxHash,
Note: "TRON event parsing not fully implemented in this version",
}, nil
}
if s.chainClient != nil {
txHashBytes := common.HexToHash(req.TxHash)
events, err := s.chainClient.ParseTransactionReceipt(ctx, txHashBytes)
if err != nil {
return nil, errs.ErrInternalServer.WrapMsg("parse tx receipt failed: " + err.Error())
}
out := make([]*pbredpacket.ParsedEvent, 0, len(events))
for _, e := range events {
data := make(map[string]string, len(e.Data))
for k, v := range e.Data {
data[k] = fmt.Sprintf("%v", v)
}
out = append(out, &pbredpacket.ParsedEvent{
Name: e.Name,
Data: data,
})
}
return &pbredpacket.ParseTxEventsResp{
Chain: "eth",
TxHash: req.TxHash,
Events: out,
}, nil
}
return nil, errs.ErrInternalServer.WrapMsg("no client available for chain: " + req.Chain)
}

@ -18,7 +18,7 @@ import (
//go:embed abi/RedPacket.json
var embeddedABI []byte
// ChainClient handles blockchain interactions for RedPacket
// ChainClient handles blockchain interactions for RedPacket.
type ChainClient struct {
client *ethclient.Client
contractABI abi.ABI
@ -28,14 +28,12 @@ type ChainClient struct {
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)
@ -74,7 +72,6 @@ func NewClient(rpcURL, contractAddress string, chainID int64, signerPrivateKey,
}, 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
@ -97,7 +94,6 @@ func (c *ChainClient) GetSignMessage(ctx context.Context, packetID *big.Int, cla
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")
@ -108,7 +104,6 @@ func (c *ChainClient) SignClaim(digest [32]byte) ([]byte, error) {
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
}
@ -116,7 +111,6 @@ func (c *ChainClient) SignClaim(digest [32]byte) ([]byte, error) {
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 {
@ -137,14 +131,22 @@ func (c *ChainClient) ChainID() *big.Int {
return new(big.Int).Set(c.chainID)
}
// Close closes the client connection
// EthClient exposes the underlying ethclient for indexers.
func (c *ChainClient) EthClient() *ethclient.Client {
return c.client
}
// ContractABI exposes the parsed ABI for indexers.
func (c *ChainClient) ContractABI() abi.ABI {
return c.contractABI
}
func (c *ChainClient) Close() {
if c.client != nil {
c.client.Close()
}
}
// ExtractABIFromEmbeddedArtifact returns the embedded contract ABI
func ExtractABIFromEmbeddedArtifact() ([]byte, error) {
if len(embeddedABI) == 0 {
return nil, fmt.Errorf("embedded ABI is empty")

@ -3,45 +3,40 @@ 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"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/tools/log"
)
// Indexer listens to blockchain events and updates database
type Indexer struct {
client *ChainClient
repo repository.Repository
db controller.RedPacketDatabase
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 {
func NewIndexer(client *ChainClient, db controller.RedPacketDatabase, pollInterval int, startBlock uint64) *Indexer {
if pollInterval <= 0 {
pollInterval = 5
}
return &Indexer{
client: client,
repo: repo,
db: db,
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...")
log.ZInfo(ctx, "starting RedPacket ETH event indexer")
go func() {
ticker := time.NewTicker(i.pollInterval)
@ -50,11 +45,11 @@ func (i *Indexer) Start(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Println("Indexer stopped")
log.ZInfo(ctx, "redpacket eth indexer stopped")
return
case <-ticker.C:
if err := i.poll(ctx); err != nil {
log.Printf("Indexer poll error: %v", err)
log.ZWarn(ctx, "redpacket eth indexer poll error", err)
}
}
}
@ -62,7 +57,6 @@ func (i *Indexer) Start(ctx context.Context) {
}
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)
@ -73,7 +67,6 @@ func (i *Indexer) poll(ctx context.Context) error {
return nil
}
// Query logs from lastBlock+1 to currentBlock
query := ethereum.FilterQuery{
FromBlock: big.NewInt(int64(i.lastBlock + 1)),
ToBlock: big.NewInt(int64(currentBlock)),
@ -85,30 +78,28 @@ func (i *Indexer) poll(ctx context.Context) error {
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
for idx := range logs {
logPtrs[idx] = &logs[idx]
}
// 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)
if err := i.processEvent(ctx, event); err != nil {
log.ZWarn(ctx, "process redpacket eth event failed", err, "event", event.Name)
}
}
i.lastBlock = currentBlock
log.Printf("✅ Indexed up to block %d, processed %d events", currentBlock, len(events))
log.ZInfo(ctx, "redpacket eth indexed", "block", currentBlock, "events", len(events))
return nil
}
func (i *Indexer) processEvent(ctx context.Context, event *ParsedEvent, logs []*types.Log) error {
func (i *Indexer) processEvent(ctx context.Context, event *ParsedEvent) error {
switch event.Name {
case "PacketCreated":
return i.handlePacketCreated(ctx, event)
@ -117,7 +108,6 @@ func (i *Indexer) processEvent(ctx context.Context, event *ParsedEvent, logs []*
case "PacketRefunded":
return i.handlePacketRefunded(ctx, event)
default:
log.Printf("Unknown event: %s", event.Name)
return nil
}
}
@ -125,11 +115,7 @@ func (i *Indexer) processEvent(ctx context.Context, event *ParsedEvent, logs []*
func (i *Indexer) handlePacketCreated(ctx context.Context, event *ParsedEvent) error {
packetID := GetPacketIDFromEvent(event)
creator := GetAddressFromEvent(event, "creator")
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
log.ZInfo(ctx, "PacketCreated event", "packetID", packetID.String(), "creator", creator.Hex())
return nil
}
@ -139,8 +125,7 @@ func (i *Indexer) handlePacketClaimed(ctx context.Context, event *ParsedEvent) e
amount := GetAmountFromEvent(event)
authNonce := GetUintFromEvent(event, "authNonce")
log.Printf("🎁 PacketClaimed: packetId=%s, claimer=%s, amount=%s",
packetID.String(), claimer.Hex(), amount.String())
log.ZInfo(ctx, "PacketClaimed event", "packetID", packetID.String(), "claimer", claimer.Hex(), "amount", amount.String())
claim := &model.RedPacketClaim{
PacketID: packetID.String(),
@ -154,26 +139,23 @@ func (i *Indexer) handlePacketClaimed(ctx context.Context, event *ParsedEvent) e
UpdatedAt: time.Now(),
}
if err := i.repo.SaveClaim(ctx, claim); err != nil {
if err := i.db.SaveClaim(ctx, claim); err != nil {
return err
}
if err := i.repo.MarkClaimAuthUsed(ctx, authNonce.String()); err != nil {
if err := i.db.MarkClaimAuthUsed(ctx, authNonce.String()); err != nil {
return err
}
return i.repo.UpdateRedPacketClaimProgress(ctx, packetID.String(), amount.String(), "")
return i.db.UpdateRedPacketClaimProgress(ctx, packetID.String(), amount.String(), "")
}
func (i *Indexer) handlePacketRefunded(ctx context.Context, event *ParsedEvent) error {
packetID := GetPacketIDFromEvent(event)
operator := GetAddressFromEvent(event, "operator")
refundTo := GetAddressFromEvent(event, "refundTo")
amount := GetAmountFromEvent(event)
log.Printf("♻️ PacketRefunded: packetId=%s, operator=%s, refundTo=%s, amount=%s",
packetID.String(), operator.Hex(), refundTo.Hex(), amount.String())
log.ZInfo(ctx, "PacketRefunded event", "packetID", packetID.String(), "refundTo", refundTo.Hex(), "amount", amount.String())
if err := i.repo.SaveRefund(ctx, &model.RedPacketRefund{
if err := i.db.SaveRefund(ctx, &model.RedPacketRefund{
PacketID: packetID.String(),
RefundTo: refundTo.Hex(),
TxHash: event.TxHash.Hex(),
@ -183,5 +165,5 @@ func (i *Indexer) handlePacketRefunded(ctx context.Context, event *ParsedEvent)
return err
}
return i.repo.UpdateRedPacketStatus(ctx, packetID.String(), "REFUNDED")
return i.db.UpdateRedPacketStatus(ctx, packetID.String(), "REFUNDED")
}

@ -9,7 +9,6 @@ import (
"github.com/ethereum/go-ethereum/core/types"
)
// ParsedEvent represents a parsed blockchain event
type ParsedEvent struct {
Name string
Data map[string]interface{}
@ -17,7 +16,6 @@ type ParsedEvent struct {
BlockNumber uint64
}
// ParseEventsFromLogs parses logs using the contract ABI
func ParseEventsFromLogs(logs []*types.Log, contractABI abi.ABI) ([]*ParsedEvent, error) {
var events []*ParsedEvent
@ -43,7 +41,6 @@ func parseEvent(log *types.Log, contractABI abi.ABI) (*ParsedEvent, error) {
data := make(map[string]interface{})
// Parse indexed parameters from topics
indexedIdx := 1
for _, arg := range event.Inputs {
if arg.Indexed {
@ -60,7 +57,6 @@ func parseEvent(log *types.Log, contractABI abi.ABI) (*ParsedEvent, error) {
}
}
// Parse non-indexed parameters from data
if len(log.Data) > 0 {
unpacked, err := event.Inputs.Unpack(log.Data)
if err == nil {
@ -87,7 +83,6 @@ func parseEvent(log *types.Log, contractABI abi.ABI) (*ParsedEvent, error) {
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 {
@ -97,7 +92,6 @@ func GetPacketIDFromEvent(event *ParsedEvent) *big.Int {
return big.NewInt(0)
}
// GetClaimerFromEvent extracts claimer address from event
func GetAddressFromEvent(event *ParsedEvent, key string) common.Address {
value, ok := event.Data[key]
if !ok {
@ -107,12 +101,10 @@ func GetAddressFromEvent(event *ParsedEvent, key string) common.Address {
return addr
}
// GetAmountFromEvent extracts amount from event
func GetAmountFromEvent(event *ParsedEvent) *big.Int {
return GetUintFromEvent(event, "amount")
}
// GetUintFromEvent extracts a uint field from event data.
func GetUintFromEvent(event *ParsedEvent, key string) *big.Int {
value, ok := event.Data[key]
if !ok {

@ -16,7 +16,6 @@ import (
"github.com/ethereum/go-ethereum/core/types"
)
// TronClient handles TRON blockchain interactions using HTTP JSON-RPC
type TronClient struct {
fullNodeURL string
contractBase58 string
@ -27,7 +26,6 @@ type TronClient struct {
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")
@ -53,6 +51,16 @@ func (t *TronClient) ContractAddress() string {
return t.contractBase58
}
// ContractBase58 exposes the contract base58 address for indexers.
func (t *TronClient) ContractBase58() string {
return t.contractBase58
}
// FullNodeURL exposes the full node URL for indexers.
func (t *TronClient) FullNodeURL() string {
return t.fullNodeURL
}
func (t *TronClient) ParseTransactionReceipt(ctx context.Context, txID string) ([]*ParsedEvent, error) {
info, err := t.getTransactionInfo(ctx, txID)
if err != nil {
@ -67,16 +75,13 @@ func (t *TronClient) ParseTransactionReceipt(ctx context.Context, txID string) (
return ParseEventsFromLogs(logs, t.parsedABI)
}
// 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))
}
@ -98,10 +103,7 @@ func (t *TronClient) SendAdminTransaction(ctx context.Context, methodName string
)
}
// 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")
}
@ -115,8 +117,6 @@ type tronTxInfoResp struct {
} `json:"log"`
}
// Helper functions
func getParamTypes(args []interface{}) string {
types := make([]string, len(args))
for i, arg := range args {
@ -134,7 +134,6 @@ func getParamTypes(args []interface{}) string {
return strings.Join(types, ",")
}
// SendTronAdminTx implements TRON transaction broadcasting (from design doc)
func SendTronAdminTx(
ctx context.Context,
fullNodeURL, ownerBase58, contractBase58, selector, methodName string,
@ -149,7 +148,6 @@ func SendTronAdminTx(
return "", err
}
// Trigger smart contract
var triggerResp map[string]interface{}
err = postJSON(ctx, fullNodeURL+"/wallet/triggersmartcontract", map[string]interface{}{
"owner_address": ownerBase58,
@ -169,7 +167,6 @@ func SendTronAdminTx(
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,
@ -179,7 +176,6 @@ func SendTronAdminTx(
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 {

@ -3,32 +3,29 @@ package chain
import (
"context"
"fmt"
"log"
"time"
"redpacket/internal/model"
"redpacket/internal/repository"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/tools/log"
)
// TronIndexer provides production-grade event listening for TRON blockchain
type TronIndexer struct {
client *TronClient
repo repository.Repository
db controller.RedPacketDatabase
pollInterval time.Duration
lastBlockNum int64 // TRON uses block numbers
lastBlockNum int64
contractAddress string
processedTxs map[string]bool // Simple dedup for this session
processedTxs map[string]bool
}
// NewTronIndexer creates a new TRON event indexer
func NewTronIndexer(client *TronClient, repo repository.Repository, pollInterval int, startBlock int64) *TronIndexer {
func NewTronIndexer(client *TronClient, db controller.RedPacketDatabase, pollInterval int, startBlock int64) *TronIndexer {
if pollInterval <= 0 {
pollInterval = 3 // TRON blocks are ~3s
pollInterval = 3
}
return &TronIndexer{
client: client,
repo: repo,
db: db,
pollInterval: time.Duration(pollInterval) * time.Second,
lastBlockNum: startBlock,
contractAddress: client.contractBase58,
@ -36,9 +33,8 @@ func NewTronIndexer(client *TronClient, repo repository.Repository, pollInterval
}
}
// Start begins polling for TRON blockchain events
func (t *TronIndexer) Start(ctx context.Context) {
log.Println("🚀 Starting TRON event indexer... (Production mode)")
log.ZInfo(ctx, "starting RedPacket TRON event indexer")
go func() {
ticker := time.NewTicker(t.pollInterval)
@ -47,12 +43,11 @@ func (t *TronIndexer) Start(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Println("TRON Indexer stopped")
log.ZInfo(ctx, "redpacket 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
log.ZWarn(ctx, "redpacket tron indexer poll error", err)
time.Sleep(2 * time.Second)
}
}
@ -61,7 +56,6 @@ func (t *TronIndexer) Start(ctx context.Context) {
}
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)
@ -71,12 +65,11 @@ func (t *TronIndexer) poll(ctx context.Context) error {
return nil
}
log.Printf("📡 TRON scanning blocks %d to %d", t.lastBlockNum+1, currentBlock)
log.ZDebug(ctx, "redpacket tron scanning blocks", "from", t.lastBlockNum+1, "to", 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)
log.ZWarn(ctx, "redpacket tron scan block failed", err, "block", blockNum)
continue
}
}
@ -104,7 +97,6 @@ func (t *TronIndexer) getNowBlock(ctx context.Context) (int64, error) {
}
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,
@ -115,7 +107,7 @@ func (t *TronIndexer) scanBlock(ctx context.Context, blockNum int64) error {
transactions, ok := blockResp["transactions"].([]interface{})
if !ok {
return nil // no transactions
return nil
}
for _, txInterface := range transactions {
@ -130,7 +122,7 @@ func (t *TronIndexer) scanBlock(ctx context.Context, blockNum int64) error {
}
if err := t.processTransaction(ctx, txID); err != nil {
log.Printf("Failed to process TRON tx %s: %v", txID, err)
log.ZWarn(ctx, "redpacket tron process tx failed", err, "txID", txID)
} else {
t.processedTxs[txID] = true
}
@ -140,7 +132,6 @@ func (t *TronIndexer) scanBlock(ctx context.Context, blockNum int64) error {
}
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,
@ -149,17 +140,14 @@ func (t *TronIndexer) processTransaction(ctx context.Context, txID string) error
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)
log.ZDebug(ctx, "redpacket tron event detected", "event", eventType, "txID", txID)
// Process different event types
switch eventType {
case "PacketCreated":
t.handleTronPacketCreated(ctx, logMap, txID)
@ -177,15 +165,11 @@ func (t *TronIndexer) processTransaction(ctx context.Context, txID string) error
}
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)
case "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0":
return "Transfer"
// Add real RedPacket event signatures here from contract
default:
return "UnknownEvent"
}
@ -194,46 +178,39 @@ func (t *TronIndexer) parseTronEvent(logEntry map[string]interface{}) string {
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
log.ZInfo(ctx, "tron PacketCreated event", "txID", txID)
}
func (t *TronIndexer) handleTronPacketClaimed(ctx context.Context, logData map[string]interface{}, txID string) {
log.Printf("🎁 [TRON] PacketClaimed event in tx %s", txID)
log.ZInfo(ctx, "tron PacketClaimed event", "txID", 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
claimer = claimerTopic
}
}
claim := &model.RedPacketClaim{
PacketID: "tron-packet-" + txID[:8], // placeholder
PacketID: "tron-packet-" + txID[:8],
ClaimerWallet: claimer,
ClaimTxHash: txID,
ClaimedAmount: amount,
Status: "CONFIRMED",
}
if err := t.repo.SaveClaim(ctx, claim); err != nil {
log.Printf("Failed to save TRON claim: %v", err)
if err := t.db.SaveClaim(ctx, claim); err != nil {
log.ZWarn(ctx, "redpacket tron save claim failed", 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
log.ZInfo(ctx, "tron PacketRefunded event", "txID", txID)
}
// GetLastProcessedBlock returns the last processed block for monitoring
func (t *TronIndexer) GetLastProcessedBlock() int64 {
return t.lastBlockNum
}

@ -0,0 +1,132 @@
package redpacket
import (
"context"
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/crypto"
"github.com/openimsdk/open-im-server/v3/internal/rpc/redpacket/chain"
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
pbredpacket "github.com/openimsdk/protocol/redpacket"
"github.com/openimsdk/tools/db/mongoutil"
"github.com/openimsdk/tools/discovery"
"github.com/openimsdk/tools/log"
"google.golang.org/grpc"
)
type Config struct {
RpcConfig config.RedPacket
MongodbConfig config.Mongo
Share config.Share
Discovery config.Discovery
}
type redPacketServer struct {
pbredpacket.UnimplementedRedPacketServer
config *Config
db controller.RedPacketDatabase
chainClient *chain.ChainClient
tronClient *chain.TronClient
signerKey *ecdsa.PrivateKey
}
func Start(ctx context.Context, conf *Config, _ discovery.SvcDiscoveryRegistry, server *grpc.Server) error {
mgoClient, err := mongoutil.NewMongoDB(ctx, conf.MongodbConfig.Build())
if err != nil {
return err
}
db := mgoClient.GetDB()
rpDB, err := mgo.NewRedPacketMongo(db)
if err != nil {
return err
}
claimDB, err := mgo.NewRedPacketClaimMongo(db)
if err != nil {
return err
}
claimAuthDB, err := mgo.NewRedPacketClaimAuthMongo(db)
if err != nil {
return err
}
refundDB, err := mgo.NewRedPacketRefundMongo(db)
if err != nil {
return err
}
challengeDB, err := mgo.NewWalletBindingChallengeMongo(db)
if err != nil {
return err
}
bindingDB, err := mgo.NewWalletBindingMongo(db)
if err != nil {
return err
}
repo := controller.NewRedPacketDatabase(rpDB, claimDB, claimAuthDB, refundDB, challengeDB, bindingDB)
chainClient, err := chain.NewClient(
conf.RpcConfig.Chain.RPCURL,
conf.RpcConfig.Chain.ContractAddress,
conf.RpcConfig.Chain.ChainID,
conf.RpcConfig.Chain.SignerPrivateKey,
conf.RpcConfig.Chain.ConfigAdminPrivateKey,
)
if err != nil {
log.ZWarn(ctx, "redpacket eth client init failed, continuing without it", err)
chainClient = nil
}
var tronClient *chain.TronClient
if conf.RpcConfig.Tron.FullNodeURL != "" {
abiJSON, abiErr := chain.ExtractABIFromEmbeddedArtifact()
if abiErr != nil {
log.ZWarn(ctx, "redpacket tron load abi failed", abiErr)
} else {
tronClient, err = chain.NewTronClient(
conf.RpcConfig.Tron.FullNodeURL,
conf.RpcConfig.Tron.ContractBase58,
conf.RpcConfig.Tron.OwnerBase58,
conf.RpcConfig.Tron.PrivateKeyHex,
abiJSON,
conf.RpcConfig.Tron.FeeLimit,
)
if err != nil {
log.ZWarn(ctx, "redpacket tron client init failed", err)
tronClient = nil
}
}
}
var signerKey *ecdsa.PrivateKey
if k := conf.RpcConfig.Chain.SignerPrivateKey; k != "" {
sk, parseErr := crypto.HexToECDSA(k)
if parseErr != nil {
log.ZWarn(ctx, "redpacket signer private key parse failed", parseErr)
} else {
signerKey = sk
}
}
srv := &redPacketServer{
config: conf,
db: repo,
chainClient: chainClient,
tronClient: tronClient,
signerKey: signerKey,
}
pbredpacket.RegisterRedPacketServer(server, srv)
if chainClient != nil {
ethIndexer := chain.NewIndexer(chainClient, repo, conf.RpcConfig.Indexer.PollInterval, 0)
ethIndexer.Start(ctx)
}
if tronClient != nil {
tronIndexer := chain.NewTronIndexer(tronClient, repo, conf.RpcConfig.Indexer.PollInterval, 0)
tronIndexer.Start(ctx)
}
return nil
}

@ -0,0 +1,777 @@
package redpacket
import (
"context"
"encoding/hex"
"fmt"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/google/uuid"
"github.com/openimsdk/open-im-server/v3/internal/rpc/redpacket/chain"
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
pbredpacket "github.com/openimsdk/protocol/redpacket"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/mcontext"
)
func (s *redPacketServer) CreateOrder(ctx context.Context, req *pbredpacket.CreateOrderReq) (*pbredpacket.CreateOrderResp, error) {
currentUserID := mcontext.GetOpUserID(ctx)
if currentUserID == "" {
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
}
bizID := uuid.NewString()
chainType, err := normalizeChainType(req.ChainType)
if err != nil {
return nil, err
}
scopeType := normalizeScopeType(req.ScopeType)
if err := validateCreateScope(scopeType, req.GroupID, req.ReceiverUserID, req.ReceiverUserIDs); err != nil {
return nil, err
}
if err := s.validateCreateHook(ctx, req); err != nil {
return nil, err
}
chainID := req.ChainID
contractAddress := strings.TrimSpace(req.ContractAddress)
if chainType == "EVM" && s.chainClient != nil {
if chainID == 0 {
if chainValue := s.chainClient.ChainID(); chainValue != nil {
chainID = chainValue.Int64()
}
}
if contractAddress == "" {
contractAddress = s.chainClient.ContractAddress().Hex()
}
}
if chainType == "TRON" && s.tronClient != nil && contractAddress == "" {
contractAddress = s.tronClient.ContractAddress()
}
rp := &model.RedPacket{
BizID: bizID,
ChainType: chainType,
ChainID: chainID,
ContractAddress: contractAddress,
CreatorUserID: currentUserID,
CreatorWallet: req.CreatorWallet,
GroupID: req.GroupID,
ScopeType: scopeType,
ReceiverUserID: req.ReceiverUserID,
ReceiverUserIDs: append([]string(nil), req.ReceiverUserIDs...),
PacketType: req.PacketType,
Token: req.Token,
TotalAmount: req.TotalAmount,
TotalShares: req.TotalShares,
ExpiryAt: req.ExpiryAt,
Status: "PENDING",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.db.CreateRedPacket(ctx, rp); err != nil {
log.ZError(ctx, "create redpacket failed", err, "bizID", bizID)
return nil, servererrs.ErrDatabase.WrapMsg("failed to create red packet")
}
return &pbredpacket.CreateOrderResp{BizID: bizID}, nil
}
func (s *redPacketServer) CreatedCallback(ctx context.Context, req *pbredpacket.CreatedCallbackReq) (*pbredpacket.CreatedCallbackResp, error) {
if strings.TrimSpace(req.BizID) == "" || strings.TrimSpace(req.TxHash) == "" {
return nil, errs.ErrArgs.WrapMsg("biz_id and tx_hash are required")
}
rp, err := s.db.GetRedPacketByBizID(ctx, req.BizID)
if err != nil {
return nil, err
}
groupID := firstNonEmpty(req.GroupID, rp.GroupID)
scopeType := normalizeScopeType(firstNonEmpty(req.ScopeType, rp.ScopeType))
receiverUserID := firstNonEmpty(req.ReceiverUserID, rp.ReceiverUserID)
receiverUserIDs := rp.ReceiverUserIDs
if len(req.ReceiverUserIDs) > 0 {
receiverUserIDs = append([]string(nil), req.ReceiverUserIDs...)
}
if err := validateCreateScope(scopeType, groupID, receiverUserID, receiverUserIDs); err != nil {
return nil, err
}
createdPacket, err := s.resolveCreatedPacket(ctx, rp, req.TxHash, req.PacketID)
if err != nil {
return nil, err
}
if err := s.db.UpdateRedPacketCreated(ctx, &model.RedPacket{
BizID: req.BizID,
ChainType: rp.ChainType,
PacketID: createdPacket.PacketID,
ChainID: createdPacket.ChainID,
ContractAddress: createdPacket.ContractAddress,
TxHash: req.TxHash,
GroupID: groupID,
ScopeType: scopeType,
ReceiverUserID: receiverUserID,
ReceiverUserIDs: receiverUserIDs,
Status: "ACTIVE",
}); err != nil {
return nil, err
}
return &pbredpacket.CreatedCallbackResp{}, nil
}
func (s *redPacketServer) GetDetail(ctx context.Context, req *pbredpacket.GetDetailReq) (*pbredpacket.GetDetailResp, error) {
if strings.TrimSpace(req.PacketID) == "" {
return nil, errs.ErrArgs.WrapMsg("packet_id is required")
}
rp, err := s.db.GetRedPacketByPacketID(ctx, req.PacketID)
if err != nil {
return nil, err
}
claims, err := s.db.GetClaimsByPacketID(ctx, req.PacketID)
if err != nil {
claims = nil
}
return &pbredpacket.GetDetailResp{
Record: redPacketModelToProto(rp),
Claims: claimsModelToProto(claims),
}, nil
}
func (s *redPacketServer) IssueClaimSign(ctx context.Context, req *pbredpacket.IssueClaimSignReq) (*pbredpacket.IssueClaimSignResp, error) {
currentUserID := mcontext.GetOpUserID(ctx)
if currentUserID == "" {
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
}
if strings.TrimSpace(req.PacketID) == "" || strings.TrimSpace(req.Claimer) == "" {
return nil, errs.ErrArgs.WrapMsg("packet_id and claimer are required")
}
if err := s.canClaim(ctx, req.PacketID, req.Claimer, currentUserID); err != nil {
return nil, err
}
packetIDBig := new(big.Int)
if _, ok := packetIDBig.SetString(req.PacketID, 10); !ok {
return nil, errs.ErrArgs.WrapMsg("invalid packet_id", "packetID", req.PacketID)
}
claimerAddr := common.HexToAddress(req.Claimer)
nonce := fmt.Sprintf("%d", time.Now().UnixNano())
authNonceBig := new(big.Int)
authNonceBig.SetString(nonce, 10)
deadline := time.Now().Add(5 * time.Minute).Unix()
randomSeedBig := new(big.Int)
if req.RandomSeed != "" && req.RandomSeed != "0" {
if _, ok := randomSeedBig.SetString(req.RandomSeed, 10); !ok {
return nil, errs.ErrArgs.WrapMsg("invalid random_seed", "randomSeed", req.RandomSeed)
}
} else {
randomSeedBig.SetInt64(time.Now().UnixNano())
}
deadlineBig := big.NewInt(deadline)
var digest [32]byte
var err error
if s.chainClient != nil {
digest, err = s.chainClient.GetSignMessage(ctx, packetIDBig, claimerAddr, authNonceBig, randomSeedBig, deadlineBig)
if err != nil {
return nil, errs.ErrInternalServer.WrapMsg("getSignMessage failed: " + err.Error())
}
} else {
digest = crypto.Keccak256Hash([]byte(fmt.Sprintf("%s:%s:%s:%s:%d", req.PacketID, req.Claimer, nonce, randomSeedBig.String(), deadline)))
}
var signature []byte
if s.signerKey != nil {
signature, err = crypto.Sign(digest[:], s.signerKey)
if err != nil {
return nil, errs.ErrInternalServer.WrapMsg("sign failed: " + err.Error())
}
if len(signature) == 65 && signature[64] < 27 {
signature[64] += 27
}
} else {
signature = []byte("0xplaceholder-signature-for-testing")
}
sigHex := "0x" + hex.EncodeToString(signature)
auth := &model.RedPacketClaimAuth{
PacketID: req.PacketID,
Claimer: req.Claimer,
AuthNonce: nonce,
RandomSeed: randomSeedBig.String(),
Deadline: deadline,
Signature: sigHex,
CreatedAt: time.Now(),
}
if err := s.db.CreateClaimAuth(ctx, auth); err != nil {
return nil, servererrs.ErrDatabase.WrapMsg("save claim auth failed: " + err.Error())
}
return &pbredpacket.IssueClaimSignResp{
AuthNonce: nonce,
Deadline: deadline,
Signature: sigHex,
RandomSeed: randomSeedBig.String(),
}, nil
}
func (s *redPacketServer) ClaimResult(ctx context.Context, req *pbredpacket.ClaimResultReq) (*pbredpacket.ClaimResultResp, error) {
currentUserID := mcontext.GetOpUserID(ctx)
if currentUserID == "" {
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
}
if strings.TrimSpace(req.PacketID) == "" || strings.TrimSpace(req.Claimer) == "" || strings.TrimSpace(req.TxHash) == "" {
return nil, errs.ErrArgs.WrapMsg("packet_id, claimer and tx_hash are required")
}
rp, err := s.db.GetRedPacketByPacketID(ctx, req.PacketID)
if err != nil {
return nil, err
}
if err := validateClaimBase(rp, currentUserID, req.Claimer); err != nil {
return nil, err
}
claim := &model.RedPacketClaim{
PacketID: req.PacketID,
UserID: currentUserID,
ClaimerWallet: req.Claimer,
ClaimTxHash: req.TxHash,
Status: "PENDING",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.db.SaveClaim(ctx, claim); err != nil {
return nil, err
}
claimedEvent, err := s.resolveClaimedEvent(ctx, rp, req.TxHash)
if err != nil {
log.ZWarn(ctx, "resolve claim event failed", err, "txHash", req.TxHash)
return &pbredpacket.ClaimResultResp{}, nil
}
if claimedEvent == nil {
return &pbredpacket.ClaimResultResp{}, nil
}
if !strings.EqualFold(claimedEvent.ClaimerWallet, req.Claimer) {
return nil, errs.ErrArgs.WrapMsg(fmt.Sprintf("claim event claimer mismatch: got %s want %s", claimedEvent.ClaimerWallet, req.Claimer))
}
confirmed := &model.RedPacketClaim{
PacketID: req.PacketID,
UserID: currentUserID,
ClaimerWallet: claimedEvent.ClaimerWallet,
AuthNonce: claimedEvent.AuthNonce,
ClaimTxHash: req.TxHash,
ClaimedAmount: claimedEvent.Amount,
BlockNumber: claimedEvent.BlockNumber,
Status: "CONFIRMED",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.db.SaveClaim(ctx, confirmed); err != nil {
return nil, err
}
if claimedEvent.AuthNonce != "" {
if err := s.db.MarkClaimAuthUsed(ctx, claimedEvent.AuthNonce); err != nil {
log.ZWarn(ctx, "mark claim auth used failed", err, "authNonce", claimedEvent.AuthNonce)
}
}
nextStatus := derivePacketStatusAfterClaim(rp, claimedEvent.Amount)
if err := s.db.UpdateRedPacketClaimProgress(ctx, req.PacketID, claimedEvent.Amount, nextStatus); err != nil {
return nil, err
}
return &pbredpacket.ClaimResultResp{}, nil
}
// canClaim runs the claim-eligibility check (formerly RedPacketService.CanClaim).
func (s *redPacketServer) canClaim(ctx context.Context, packetID, claimer, userID string) error {
rp, err := s.db.GetRedPacketByPacketID(ctx, packetID)
if err != nil {
return err
}
if err := validateClaimBase(rp, userID, claimer); err != nil {
return err
}
if err := s.ensureWalletBinding(ctx, userID, claimer, rp.ChainType); err != nil {
return err
}
switch rp.PacketType {
case 0:
return s.validateFixedPacketClaim(ctx, rp, userID, claimer)
case 1:
return s.validateRandomPacketClaim(ctx, rp, userID, claimer)
case 2:
return s.validateTransferPacketClaim(ctx, rp, userID, claimer)
default:
return errs.ErrArgs.WrapMsg(fmt.Sprintf("unsupported packet_type: %d", rp.PacketType))
}
}
type claimedEventSnapshot struct {
ClaimerWallet string
AuthNonce string
Amount string
BlockNumber uint64
}
type createdPacketSnapshot struct {
PacketID string
ChainID int64
ContractAddress string
CreatorWallet string
PacketType int32
Token string
TotalAmount string
TotalShares int32
ExpiryAt int64
}
func (s *redPacketServer) resolveCreatedPacket(ctx context.Context, rp *model.RedPacket, txHashHex, fallbackPacketID string) (*createdPacketSnapshot, error) {
switch rp.ChainType {
case "EVM":
if s.chainClient == nil {
if fallbackPacketID == "" {
return nil, errs.ErrArgs.WrapMsg("packet_id is required when EVM client is unavailable")
}
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
}
events, err := s.chainClient.ParseTransactionReceipt(ctx, common.HexToHash(txHashHex))
if err != nil {
if fallbackPacketID == "" {
return nil, errs.ErrInternalServer.WrapMsg("parse created tx failed: " + err.Error())
}
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
}
for _, event := range events {
if event.Name != "PacketCreated" {
continue
}
createdPacket := buildCreatedPacketSnapshot(rp, event)
if chainValue := s.chainClient.ChainID(); chainValue != nil {
createdPacket.ChainID = chainValue.Int64()
}
createdPacket.ContractAddress = s.chainClient.ContractAddress().Hex()
if err := validateCreatedPacket(rp, createdPacket); err != nil {
return nil, err
}
return createdPacket, nil
}
if fallbackPacketID == "" {
return nil, errs.ErrInternalServer.WrapMsg("PacketCreated event not found in tx: " + txHashHex)
}
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
case "TRON":
if s.tronClient == nil {
if fallbackPacketID == "" {
return nil, errs.ErrArgs.WrapMsg("packet_id is required when TRON client is unavailable")
}
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
}
events, err := s.tronClient.ParseTransactionReceipt(ctx, txHashHex)
if err != nil {
if fallbackPacketID == "" {
return nil, errs.ErrInternalServer.WrapMsg("parse tron created tx failed: " + err.Error())
}
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
}
for _, event := range events {
if event.Name != "PacketCreated" {
continue
}
createdPacket := buildCreatedPacketSnapshot(rp, event)
createdPacket.ContractAddress = firstNonEmpty(s.tronClient.ContractAddress(), rp.ContractAddress)
if err := validateCreatedPacket(rp, createdPacket); err != nil {
return nil, err
}
return createdPacket, nil
}
if fallbackPacketID == "" {
return nil, errs.ErrInternalServer.WrapMsg("PacketCreated event not found in TRON tx: " + txHashHex)
}
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
default:
return nil, errs.ErrArgs.WrapMsg("unsupported chain_type: " + rp.ChainType)
}
}
// validateCreateHook reserves a centralized validation extension point split by packet type.
func (s *redPacketServer) validateCreateHook(ctx context.Context, req *pbredpacket.CreateOrderReq) error {
switch req.PacketType {
case 0:
return s.validateFixedPacketCreate(ctx, req)
case 1:
return s.validateRandomPacketCreate(ctx, req)
case 2:
return s.validateTransferPacketCreate(ctx, req)
default:
return errs.ErrArgs.WrapMsg(fmt.Sprintf("unsupported packet_type: %d", req.PacketType))
}
}
func (s *redPacketServer) validateFixedPacketCreate(ctx context.Context, req *pbredpacket.CreateOrderReq) error {
return nil
}
func (s *redPacketServer) validateRandomPacketCreate(ctx context.Context, req *pbredpacket.CreateOrderReq) error {
return nil
}
func (s *redPacketServer) validateTransferPacketCreate(ctx context.Context, req *pbredpacket.CreateOrderReq) error {
return nil
}
func buildFallbackCreatedPacket(rp *model.RedPacket, packetID string) *createdPacketSnapshot {
return &createdPacketSnapshot{
PacketID: packetID,
ChainID: rp.ChainID,
ContractAddress: rp.ContractAddress,
CreatorWallet: strings.ToLower(rp.CreatorWallet),
PacketType: rp.PacketType,
Token: normalizeTokenAddress(rp.Token),
TotalAmount: rp.TotalAmount,
TotalShares: rp.TotalShares,
ExpiryAt: rp.ExpiryAt,
}
}
func buildCreatedPacketSnapshot(rp *model.RedPacket, event *chain.ParsedEvent) *createdPacketSnapshot {
return &createdPacketSnapshot{
PacketID: chain.GetPacketIDFromEvent(event).String(),
ChainID: rp.ChainID,
ContractAddress: rp.ContractAddress,
CreatorWallet: strings.ToLower(chain.GetAddressFromEvent(event, "creator").Hex()),
PacketType: int32(chain.GetUintFromEvent(event, "packetType").Int64()),
Token: strings.ToLower(chain.GetAddressFromEvent(event, "token").Hex()),
TotalAmount: chain.GetUintFromEvent(event, "totalAmount").String(),
TotalShares: int32(chain.GetUintFromEvent(event, "totalShares").Int64()),
ExpiryAt: chain.GetUintFromEvent(event, "expiryAt").Int64(),
}
}
func validateCreatedPacket(rp *model.RedPacket, createdPacket *createdPacketSnapshot) error {
if createdPacket == nil {
return errs.ErrInternalServer.WrapMsg("created packet is nil")
}
if createdPacket.CreatorWallet != "" && strings.ToLower(rp.CreatorWallet) != createdPacket.CreatorWallet {
return errs.ErrArgs.WrapMsg(fmt.Sprintf("creator mismatch: got %s want %s", createdPacket.CreatorWallet, rp.CreatorWallet))
}
if createdPacket.PacketType != rp.PacketType {
return errs.ErrArgs.WrapMsg(fmt.Sprintf("packet type mismatch: got %d want %d", createdPacket.PacketType, rp.PacketType))
}
if createdPacket.TotalAmount != rp.TotalAmount {
return errs.ErrArgs.WrapMsg(fmt.Sprintf("total amount mismatch: got %s want %s", createdPacket.TotalAmount, rp.TotalAmount))
}
if createdPacket.TotalShares != rp.TotalShares {
return errs.ErrArgs.WrapMsg(fmt.Sprintf("total shares mismatch: got %d want %d", createdPacket.TotalShares, rp.TotalShares))
}
expectedToken := normalizeTokenAddress(rp.Token)
if createdPacket.Token != expectedToken {
return errs.ErrArgs.WrapMsg(fmt.Sprintf("token mismatch: got %s want %s", createdPacket.Token, expectedToken))
}
if rp.ExpiryAt > 0 && createdPacket.ExpiryAt != rp.ExpiryAt {
return errs.ErrArgs.WrapMsg(fmt.Sprintf("expiry mismatch: got %d want %d", createdPacket.ExpiryAt, rp.ExpiryAt))
}
return nil
}
func validateClaimBase(rp *model.RedPacket, userID, claimer string) error {
if rp == nil {
return servererrs.ErrRecordNotFound.WrapMsg("packet not found")
}
if strings.TrimSpace(userID) == "" {
return errs.ErrArgs.WrapMsg("user_id is required")
}
if strings.TrimSpace(claimer) == "" {
return errs.ErrArgs.WrapMsg("claimer is required")
}
if rp.Status != "ACTIVE" {
return errs.ErrArgs.WrapMsg("packet is not active, current status: " + rp.Status)
}
if rp.ExpiryAt > 0 && rp.ExpiryAt <= time.Now().Unix() {
return errs.ErrArgs.WrapMsg("packet is expired")
}
if rp.Status == "REFUNDED" {
return errs.ErrArgs.WrapMsg("packet is refunded")
}
return nil
}
func (s *redPacketServer) validateFixedPacketClaim(ctx context.Context, rp *model.RedPacket, userID, claimer string) error {
if strings.TrimSpace(rp.GroupID) == "" {
return errs.ErrArgs.WrapMsg("group_id is required for fixed packet claim")
}
if err := s.ensureNotClaimed(ctx, rp.PacketID, userID, claimer); err != nil {
return err
}
return s.ensureGroupEligibility(ctx, rp.GroupID, userID)
}
func (s *redPacketServer) validateRandomPacketClaim(ctx context.Context, rp *model.RedPacket, userID, claimer string) error {
if strings.TrimSpace(rp.GroupID) == "" {
return errs.ErrArgs.WrapMsg("group_id is required for random packet claim")
}
if err := s.ensureNotClaimed(ctx, rp.PacketID, userID, claimer); err != nil {
return err
}
return s.ensureGroupEligibility(ctx, rp.GroupID, userID)
}
func (s *redPacketServer) validateTransferPacketClaim(ctx context.Context, rp *model.RedPacket, userID, claimer string) error {
if err := s.ensureNotClaimed(ctx, rp.PacketID, userID, claimer); err != nil {
return err
}
if strings.TrimSpace(rp.ReceiverUserID) == "" {
return errs.ErrArgs.WrapMsg("receiver_user_id is required for transfer claim")
}
if rp.ReceiverUserID != userID {
return errs.ErrNoPermission.WrapMsg("user is not the designated receiver")
}
return s.ensureFriendRelationship(ctx, rp.CreatorUserID, userID)
}
func (s *redPacketServer) ensureNotClaimed(ctx context.Context, packetID, userID, claimer string) error {
if strings.TrimSpace(userID) != "" {
claim, err := s.db.GetClaimByPacketIDAndUserID(ctx, packetID, userID)
if err == nil && claim != nil && claim.Status != "FAILED" {
return errs.ErrArgs.WrapMsg("user already claimed")
}
if err != nil && !errs.ErrRecordNotFound.Is(err) {
return err
}
}
claim, err := s.db.GetClaimByPacketIDAndClaimer(ctx, packetID, claimer)
if err == nil && claim != nil && claim.Status != "FAILED" {
return errs.ErrArgs.WrapMsg("already claimed")
}
if err != nil && !errs.ErrRecordNotFound.Is(err) {
return err
}
return nil
}
func (s *redPacketServer) ensureWalletBinding(ctx context.Context, userID, claimer, chainType string) error {
if _, err := s.db.GetActiveWalletBinding(ctx, userID, chainType, claimer); err != nil {
if errs.ErrRecordNotFound.Is(err) {
return errs.ErrNoPermission.WrapMsg("wallet is not bound to user")
}
return err
}
return nil
}
// ensureGroupEligibility reserves centralized group membership checks.
func (s *redPacketServer) ensureGroupEligibility(ctx context.Context, groupID, userID string) error {
return nil
}
// ensureFriendRelationship reserves centralized relation validation for transfer packets.
func (s *redPacketServer) ensureFriendRelationship(ctx context.Context, creatorUserID, receiverUserID string) error {
return nil
}
func (s *redPacketServer) resolveClaimedEvent(ctx context.Context, rp *model.RedPacket, txHash string) (*claimedEventSnapshot, error) {
var (
events []*chain.ParsedEvent
err error
)
switch rp.ChainType {
case "EVM":
if s.chainClient == nil {
return nil, nil
}
events, err = s.chainClient.ParseTransactionReceipt(ctx, common.HexToHash(txHash))
case "TRON":
if s.tronClient == nil {
return nil, nil
}
events, err = s.tronClient.ParseTransactionReceipt(ctx, txHash)
default:
return nil, errs.ErrArgs.WrapMsg("unsupported chain_type: " + rp.ChainType)
}
if err != nil {
return nil, err
}
for _, event := range events {
if event.Name != "PacketClaimed" {
continue
}
packetID := chain.GetPacketIDFromEvent(event).String()
claimerWallet := strings.ToLower(chain.GetAddressFromEvent(event, "claimer").Hex())
if packetID != rp.PacketID {
return nil, errs.ErrArgs.WrapMsg(fmt.Sprintf("claim event packet mismatch: got %s want %s", packetID, rp.PacketID))
}
return &claimedEventSnapshot{
ClaimerWallet: claimerWallet,
AuthNonce: chain.GetUintFromEvent(event, "authNonce").String(),
Amount: chain.GetAmountFromEvent(event).String(),
BlockNumber: event.BlockNumber,
}, nil
}
return nil, nil
}
func derivePacketStatusAfterClaim(rp *model.RedPacket, claimedAmount string) string {
if rp == nil {
return ""
}
if rp.PacketType == 2 {
return "COMPLETED"
}
nextShares := rp.ClaimedShares + 1
if rp.TotalShares > 0 && nextShares >= rp.TotalShares {
return "COMPLETED"
}
totalClaimed := addNumericStrings(rp.ClaimedAmount, claimedAmount)
if rp.TotalAmount != "" && totalClaimed == rp.TotalAmount {
return "COMPLETED"
}
return "ACTIVE"
}
func addNumericStrings(current, delta string) string {
left := new(big.Int)
if current != "" {
left.SetString(current, 10)
}
right := new(big.Int)
if delta != "" {
right.SetString(delta, 10)
}
return new(big.Int).Add(left, right).String()
}
func normalizeScopeType(scopeType string) string {
switch strings.ToUpper(strings.TrimSpace(scopeType)) {
case "GROUP", "DIRECT", "PUBLIC":
return strings.ToUpper(strings.TrimSpace(scopeType))
default:
return "PUBLIC"
}
}
func normalizeChainType(chainType string) (string, error) {
switch strings.ToUpper(strings.TrimSpace(chainType)) {
case "EVM":
return "EVM", nil
case "TRON":
return "TRON", nil
default:
return "", errs.ErrArgs.WrapMsg("unsupported chain_type: " + chainType)
}
}
func validateCreateScope(scopeType, groupID, receiverUserID string, receiverUserIDs []string) error {
switch scopeType {
case "GROUP":
if strings.TrimSpace(groupID) == "" {
return errs.ErrArgs.WrapMsg("group_id is required when scope_type=GROUP")
}
case "DIRECT":
if strings.TrimSpace(receiverUserID) == "" && len(receiverUserIDs) == 0 {
return errs.ErrArgs.WrapMsg("receiver_user_id or receiver_user_ids is required when scope_type=DIRECT")
}
}
return nil
}
func normalizeTokenAddress(token string) string {
if strings.TrimSpace(token) == "" {
return strings.ToLower(common.Address{}.Hex())
}
return strings.ToLower(common.HexToAddress(token).Hex())
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func redPacketModelToProto(rp *model.RedPacket) *pbredpacket.RedPacketRecord {
if rp == nil {
return nil
}
return &pbredpacket.RedPacketRecord{
BizID: rp.BizID,
ChainType: rp.ChainType,
PacketID: rp.PacketID,
ChainID: rp.ChainID,
ContractAddress: rp.ContractAddress,
CreatorUserID: rp.CreatorUserID,
CreatorWallet: rp.CreatorWallet,
GroupID: rp.GroupID,
ScopeType: rp.ScopeType,
ReceiverUserID: rp.ReceiverUserID,
ReceiverUserIDs: append([]string(nil), rp.ReceiverUserIDs...),
PacketType: rp.PacketType,
Token: rp.Token,
TotalAmount: rp.TotalAmount,
TotalShares: rp.TotalShares,
ClaimedAmount: rp.ClaimedAmount,
ClaimedShares: rp.ClaimedShares,
ExpiryAt: rp.ExpiryAt,
TxHash: rp.TxHash,
Status: rp.Status,
CreatedAt: rp.CreatedAt.Unix(),
UpdatedAt: rp.UpdatedAt.Unix(),
}
}
func claimsModelToProto(claims []*model.RedPacketClaim) []*pbredpacket.RedPacketClaimRecord {
out := make([]*pbredpacket.RedPacketClaimRecord, 0, len(claims))
for _, c := range claims {
if c == nil {
continue
}
out = append(out, &pbredpacket.RedPacketClaimRecord{
PacketID: c.PacketID,
UserID: c.UserID,
ClaimerWallet: c.ClaimerWallet,
AuthNonce: c.AuthNonce,
ClaimTxHash: c.ClaimTxHash,
ClaimedAmount: c.ClaimedAmount,
BlockNumber: c.BlockNumber,
Status: c.Status,
CreatedAt: c.CreatedAt.Unix(),
UpdatedAt: c.UpdatedAt.Unix(),
})
}
return out
}

@ -0,0 +1,251 @@
package redpacket
import (
"context"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/google/uuid"
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
pbredpacket "github.com/openimsdk/protocol/redpacket"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/mcontext"
)
func (s *redPacketServer) IssueWalletBindChallenge(ctx context.Context, req *pbredpacket.IssueWalletBindChallengeReq) (*pbredpacket.IssueWalletBindChallengeResp, error) {
currentUserID := mcontext.GetOpUserID(ctx)
if currentUserID == "" {
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
}
chainType, err := normalizeChainType(req.ChainType)
if err != nil {
return nil, err
}
walletAddress := strings.TrimSpace(req.WalletAddress)
if walletAddress == "" {
return nil, errs.ErrArgs.WrapMsg("wallet_address is required")
}
challengeID := uuid.NewString()
nonce := uuid.NewString()
issuedAt := time.Now().UTC()
expiresAt := issuedAt.Add(10 * time.Minute)
protocol := "siwe-eip4361"
signMethod := "personal_sign"
message := buildEVMBindMessage(currentUserID, walletAddress, req.Domain, req.Uri, req.ChainID, challengeID, nonce, issuedAt, expiresAt)
if chainType == "TRON" {
protocol = "tron-signmessagev2"
signMethod = "signMessageV2"
message = buildTRONBindMessage(currentUserID, walletAddress, req.ChainID, challengeID, nonce, issuedAt, expiresAt)
}
challenge := &model.WalletBindingChallenge{
ChallengeID: challengeID,
UserID: currentUserID,
ChainType: chainType,
ChainID: req.ChainID,
WalletAddress: walletAddress,
Nonce: nonce,
Message: message,
Protocol: protocol,
SignMethod: signMethod,
Status: "PENDING",
ExpiresAt: expiresAt,
CreatedAt: issuedAt,
UpdatedAt: issuedAt,
}
if err := s.db.CreateWalletBindingChallenge(ctx, challenge); err != nil {
return nil, err
}
return &pbredpacket.IssueWalletBindChallengeResp{
ChallengeID: challengeID,
UserID: currentUserID,
ChainType: chainType,
ChainID: req.ChainID,
Wallet: walletAddress,
Protocol: protocol,
SignMethod: signMethod,
Nonce: nonce,
Message: message,
IssuedAt: issuedAt.Format(time.RFC3339),
ExpiresAt: expiresAt.Format(time.RFC3339),
}, nil
}
func (s *redPacketServer) ConfirmWalletBind(ctx context.Context, req *pbredpacket.ConfirmWalletBindReq) (*pbredpacket.ConfirmWalletBindResp, error) {
if strings.TrimSpace(req.ChallengeID) == "" || strings.TrimSpace(req.Signature) == "" {
return nil, errs.ErrArgs.WrapMsg("challenge_id and signature are required")
}
challenge, err := s.db.GetWalletBindingChallenge(ctx, req.ChallengeID)
if err != nil {
return nil, err
}
if challenge.Status != "PENDING" {
return nil, errs.ErrArgs.WrapMsg("challenge is not pending")
}
if time.Now().UTC().After(challenge.ExpiresAt) {
challenge.Status = "EXPIRED"
challenge.UpdatedAt = time.Now()
_ = s.db.UpdateWalletBindingChallenge(ctx, challenge)
return nil, errs.ErrArgs.WrapMsg("challenge is expired")
}
switch challenge.ChainType {
case "EVM":
if err := verifyEVMBindSignature(challenge.Message, challenge.WalletAddress, req.Signature); err != nil {
challenge.Status = "FAILED"
challenge.Signature = req.Signature
challenge.UpdatedAt = time.Now()
_ = s.db.UpdateWalletBindingChallenge(ctx, challenge)
return nil, err
}
case "TRON":
return nil, errs.ErrInternalServer.WrapMsg("TRON wallet binding verification is not implemented yet")
default:
return nil, errs.ErrArgs.WrapMsg("unsupported chain_type: " + challenge.ChainType)
}
now := time.Now().UTC()
challenge.Status = "VERIFIED"
challenge.Signature = req.Signature
challenge.VerifiedAt = &now
challenge.UpdatedAt = now
if err := s.db.UpdateWalletBindingChallenge(ctx, challenge); err != nil {
return nil, err
}
binding := &model.WalletBinding{
UserID: challenge.UserID,
ChainType: challenge.ChainType,
ChainID: challenge.ChainID,
WalletAddress: challenge.WalletAddress,
Status: "ACTIVE",
ChallengeID: challenge.ChallengeID,
VerifiedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
if err := s.db.UpsertWalletBinding(ctx, binding); err != nil {
return nil, err
}
return &pbredpacket.ConfirmWalletBindResp{
UserID: binding.UserID,
ChainType: binding.ChainType,
ChainID: binding.ChainID,
WalletAddress: binding.WalletAddress,
Status: binding.Status,
VerifiedAt: binding.VerifiedAt.Format(time.RFC3339),
}, nil
}
func (s *redPacketServer) GetWalletBinding(ctx context.Context, req *pbredpacket.GetWalletBindingReq) (*pbredpacket.GetWalletBindingResp, error) {
currentUserID := mcontext.GetOpUserID(ctx)
if currentUserID == "" {
return nil, servererrs.ErrNoPermission.WrapMsg("op user id is empty")
}
normalizedChainType, err := normalizeChainType(req.ChainType)
if err != nil {
return nil, err
}
binding, err := s.db.GetActiveWalletBinding(ctx, currentUserID, normalizedChainType, req.WalletAddress)
if err != nil {
return nil, err
}
return &pbredpacket.GetWalletBindingResp{
UserID: binding.UserID,
ChainType: binding.ChainType,
ChainID: binding.ChainID,
WalletAddress: binding.WalletAddress,
Status: binding.Status,
ChallengeID: binding.ChallengeID,
VerifiedAt: binding.VerifiedAt.Format(time.RFC3339),
}, nil
}
func buildEVMBindMessage(userID, walletAddress, domainIn, uriIn string, chainID int64, challengeID, nonce string, issuedAt, expiresAt time.Time) string {
domain := strings.TrimSpace(domainIn)
if domain == "" {
domain = "redpacket"
}
uri := strings.TrimSpace(uriIn)
if uri == "" {
uri = "https://redpacket.local/wallet-bind"
}
var b strings.Builder
fmt.Fprintf(&b, "%s wants you to sign in with your Ethereum account:\n", domain)
b.WriteString(strings.TrimSpace(walletAddress))
b.WriteString("\n\n")
fmt.Fprintf(&b, "Bind wallet %s to user %s.\n", strings.TrimSpace(walletAddress), strings.TrimSpace(userID))
fmt.Fprintf(&b, "URI: %s\n", uri)
fmt.Fprintf(&b, "Version: 1\n")
fmt.Fprintf(&b, "Chain ID: %d\n", chainID)
fmt.Fprintf(&b, "Nonce: %s\n", nonce)
fmt.Fprintf(&b, "Issued At: %s\n", issuedAt.Format(time.RFC3339))
fmt.Fprintf(&b, "Expiration Time: %s\n", expiresAt.Format(time.RFC3339))
fmt.Fprintf(&b, "Request ID: %s", challengeID)
return b.String()
}
func buildTRONBindMessage(userID, walletAddress string, chainID int64, challengeID, nonce string, issuedAt, expiresAt time.Time) string {
return fmt.Sprintf(
"Bind TRON wallet %s to user %s\nchallenge_id: %s\nnonce: %s\nchain_id: %d\nissued_at: %s\nexpires_at: %s",
strings.TrimSpace(walletAddress),
strings.TrimSpace(userID),
challengeID,
nonce,
chainID,
issuedAt.Format(time.RFC3339),
expiresAt.Format(time.RFC3339),
)
}
func verifyEVMBindSignature(message, walletAddress, signature string) error {
if strings.TrimSpace(message) == "" {
return errs.ErrArgs.WrapMsg("bind message is empty")
}
if !common.IsHexAddress(walletAddress) {
return errs.ErrArgs.WrapMsg("invalid evm wallet address")
}
sig, err := hex.DecodeString(strings.TrimPrefix(signature, "0x"))
if err != nil {
return errs.ErrArgs.WrapMsg("decode signature failed: " + err.Error())
}
if len(sig) != 65 {
return errs.ErrArgs.WrapMsg(fmt.Sprintf("invalid signature length: %d", len(sig)))
}
if sig[64] >= 27 {
sig[64] -= 27
}
if sig[64] > 1 {
return errs.ErrArgs.WrapMsg("invalid signature recovery id")
}
hash := crypto.Keccak256Hash([]byte(personalSignMessage(message)))
pubKey, err := crypto.SigToPub(hash.Bytes(), sig)
if err != nil {
return errs.ErrInternalServer.WrapMsg("recover signer failed: " + err.Error())
}
recovered := crypto.PubkeyToAddress(*pubKey)
if !strings.EqualFold(recovered.Hex(), walletAddress) {
return errs.ErrNoPermission.WrapMsg("signature does not match wallet address")
}
return nil
}
func personalSignMessage(message string) string {
return fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message)
}

@ -45,6 +45,7 @@ var (
OpenIMRPCUserCfgFileName string
OpenIMRPCRtcCfgFileName string
OpenIMRPCCryptoCfgFileName string
OpenIMRPCRedPacketCfgFileName string
DiscoveryConfigFilename string
)
@ -77,6 +78,7 @@ func init() {
OpenIMRPCUserCfgFileName = "openim-rpc-user.yml"
OpenIMRPCRtcCfgFileName = "openim-rpc-rtc.yml"
OpenIMRPCCryptoCfgFileName = "openim-rpc-crypto.yml"
OpenIMRPCRedPacketCfgFileName = "openim-rpc-redpacket.yml"
DiscoveryConfigFilename = "discovery.yml"
ConfigEnvPrefixMap = make(map[string]string)
@ -87,7 +89,8 @@ func init() {
OpenIMAPICfgFileName, OpenIMCronTaskCfgFileName, OpenIMMsgGatewayCfgFileName,
OpenIMMsgTransferCfgFileName, OpenIMPushCfgFileName, OpenIMCaptchaCfgFileName, OpenIMRPCAuthCfgFileName, OpenIMRPCCaptchaCfgFileName,
OpenIMRPCConversationCfgFileName, OpenIMRPCFriendCfgFileName, OpenIMRPCGroupCfgFileName,
OpenIMRPCMsgCfgFileName, OpenIMRPCThirdCfgFileName, OpenIMRPCUserCfgFileName, OpenIMRPCRtcCfgFileName, OpenIMRPCCryptoCfgFileName, DiscoveryConfigFilename,
OpenIMRPCMsgCfgFileName, OpenIMRPCThirdCfgFileName, OpenIMRPCUserCfgFileName, OpenIMRPCRtcCfgFileName, OpenIMRPCCryptoCfgFileName,
OpenIMRPCRedPacketCfgFileName, DiscoveryConfigFilename,
}
for _, fileName := range fileNames {

@ -0,0 +1,47 @@
package cmd
import (
"context"
"github.com/openimsdk/open-im-server/v3/internal/rpc/redpacket"
"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc"
"github.com/openimsdk/open-im-server/v3/version"
"github.com/openimsdk/tools/system/program"
"github.com/spf13/cobra"
)
type RedPacketRpcCmd struct {
*RootCmd
ctx context.Context
configMap map[string]any
redPacketConfig *redpacket.Config
}
func NewRedPacketRpcCmd() *RedPacketRpcCmd {
var redPacketConfig redpacket.Config
ret := &RedPacketRpcCmd{redPacketConfig: &redPacketConfig}
ret.configMap = map[string]any{
OpenIMRPCRedPacketCfgFileName: &redPacketConfig.RpcConfig,
MongodbConfigFileName: &redPacketConfig.MongodbConfig,
ShareFileName: &redPacketConfig.Share,
DiscoveryConfigFilename: &redPacketConfig.Discovery,
}
ret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))
ret.ctx = context.WithValue(context.Background(), "version", version.Version)
ret.Command.RunE = func(cmd *cobra.Command, args []string) error {
return ret.runE()
}
return ret
}
func (c *RedPacketRpcCmd) Exec() error {
return c.Execute()
}
func (c *RedPacketRpcCmd) runE() error {
return startrpc.Start(c.ctx, &c.redPacketConfig.Discovery, &c.redPacketConfig.RpcConfig.Prometheus, c.redPacketConfig.RpcConfig.RPC.ListenIP,
c.redPacketConfig.RpcConfig.RPC.RegisterIP, c.redPacketConfig.RpcConfig.RPC.AutoSetPorts, c.redPacketConfig.RpcConfig.RPC.Ports,
c.Index(), c.redPacketConfig.Share.RpcRegisterName.RedPacket, &c.redPacketConfig.Share, c.redPacketConfig,
nil,
redpacket.Start)
}

@ -427,6 +427,7 @@ type RpcRegisterName struct {
Captcha string `mapstructure:"captcha"`
Rtc string `mapstructure:"rtc"`
Crypto string `mapstructure:"crypto"`
RedPacket string `mapstructure:"redPacket"`
}
func (r *RpcRegisterName) GetServiceNames() []string {
@ -443,6 +444,7 @@ func (r *RpcRegisterName) GetServiceNames() []string {
r.Captcha,
r.Rtc,
r.Crypto,
r.RedPacket,
}
}
@ -482,6 +484,39 @@ type VirgilConfig struct {
AppKeyID string `mapstructure:"appKeyID"`
}
type RedPacket struct {
RPC struct {
RegisterIP string `mapstructure:"registerIP"`
ListenIP string `mapstructure:"listenIP"`
AutoSetPorts bool `mapstructure:"autoSetPorts"`
Ports []int `mapstructure:"ports"`
} `mapstructure:"rpc"`
Prometheus Prometheus `mapstructure:"prometheus"`
Chain RedPacketChain `mapstructure:"chain"`
Tron RedPacketTron `mapstructure:"tron"`
Indexer RedPacketIndexer `mapstructure:"indexer"`
}
type RedPacketChain struct {
RPCURL string `mapstructure:"rpcURL"`
ContractAddress string `mapstructure:"contractAddress"`
ChainID int64 `mapstructure:"chainID"`
SignerPrivateKey string `mapstructure:"signerPrivateKey"`
ConfigAdminPrivateKey string `mapstructure:"configAdminPrivateKey"`
}
type RedPacketTron struct {
FullNodeURL string `mapstructure:"fullNodeURL"`
ContractBase58 string `mapstructure:"contractBase58"`
OwnerBase58 string `mapstructure:"ownerBase58"`
PrivateKeyHex string `mapstructure:"privateKeyHex"`
FeeLimit int64 `mapstructure:"feeLimit"`
}
type RedPacketIndexer struct {
PollInterval int `mapstructure:"pollInterval"`
}
// FullConfig stores all configurations for before and after events
type Webhooks struct {
@ -694,6 +729,7 @@ var (
OpenIMRPCUserCfgFileName = "openim-rpc-user.yml"
OpenIMRPCRtcCfgFileName = "openim-rpc-rtc.yml"
OpenIMRPCCryptoCfgFileName = "openim-rpc-crypto.yml"
OpenIMRPCRedPacketCfgFileName = "openim-rpc-redpacket.yml"
RedisConfigFileName = "redis.yml"
ShareFileName = "share.yml"
WebhooksConfigFileName = "webhooks.yml"
@ -787,6 +823,10 @@ func (c *Crypto) GetConfigFileName() string {
return OpenIMRPCCryptoCfgFileName
}
func (rp *RedPacket) GetConfigFileName() string {
return OpenIMRPCRedPacketCfgFileName
}
func (r *Redis) GetConfigFileName() string {
return RedisConfigFileName
}

@ -0,0 +1,141 @@
package controller
import (
"context"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
)
// RedPacketDatabase is a façade aggregating all redpacket-related collections.
// It mirrors the legacy Repository interface so the rpc service layer stays
// unaware of the underlying storage.
type RedPacketDatabase interface {
CreateRedPacket(ctx context.Context, rp *model.RedPacket) error
GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error)
GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error)
UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error
UpdateRedPacketStatus(ctx context.Context, packetID, status string) error
UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error
CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error
GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error)
MarkClaimAuthUsed(ctx context.Context, authNonce string) error
SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error
GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error)
GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error)
GetClaimsByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error)
SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error
CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error)
UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error
GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error)
}
type redPacketDatabase struct {
rp database.RedPacket
claim database.RedPacketClaim
claimAuth database.RedPacketClaimAuth
refund database.RedPacketRefund
challenge database.WalletBindingChallenge
binding database.WalletBinding
}
func NewRedPacketDatabase(
rp database.RedPacket,
claim database.RedPacketClaim,
claimAuth database.RedPacketClaimAuth,
refund database.RedPacketRefund,
challenge database.WalletBindingChallenge,
binding database.WalletBinding,
) RedPacketDatabase {
return &redPacketDatabase{
rp: rp,
claim: claim,
claimAuth: claimAuth,
refund: refund,
challenge: challenge,
binding: binding,
}
}
func (d *redPacketDatabase) CreateRedPacket(ctx context.Context, rp *model.RedPacket) error {
return d.rp.Create(ctx, rp)
}
func (d *redPacketDatabase) GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error) {
return d.rp.GetByBizID(ctx, bizID)
}
func (d *redPacketDatabase) GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error) {
return d.rp.GetByPacketID(ctx, packetID)
}
func (d *redPacketDatabase) UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error {
return d.rp.UpdateCreated(ctx, rp)
}
func (d *redPacketDatabase) UpdateRedPacketStatus(ctx context.Context, packetID, status string) error {
return d.rp.UpdateStatus(ctx, packetID, status)
}
func (d *redPacketDatabase) UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error {
return d.rp.UpdateClaimProgress(ctx, packetID, claimedAmount, status)
}
func (d *redPacketDatabase) CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error {
return d.claimAuth.Create(ctx, auth)
}
func (d *redPacketDatabase) GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error) {
return d.claimAuth.Get(ctx, packetID, claimer)
}
func (d *redPacketDatabase) MarkClaimAuthUsed(ctx context.Context, authNonce string) error {
return d.claimAuth.MarkUsed(ctx, authNonce)
}
func (d *redPacketDatabase) SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error {
return d.claim.Save(ctx, claim)
}
func (d *redPacketDatabase) GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error) {
return d.claim.GetByPacketIDAndClaimer(ctx, packetID, claimer)
}
func (d *redPacketDatabase) GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error) {
return d.claim.GetByPacketIDAndUserID(ctx, packetID, userID)
}
func (d *redPacketDatabase) GetClaimsByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error) {
return d.claim.ListByPacketID(ctx, packetID)
}
func (d *redPacketDatabase) SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error {
return d.refund.Save(ctx, refund)
}
func (d *redPacketDatabase) CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
return d.challenge.Create(ctx, challenge)
}
func (d *redPacketDatabase) GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error) {
return d.challenge.Get(ctx, challengeID)
}
func (d *redPacketDatabase) UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
return d.challenge.Update(ctx, challenge)
}
func (d *redPacketDatabase) UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error {
return d.binding.Upsert(ctx, binding)
}
func (d *redPacketDatabase) GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error) {
return d.binding.GetActive(ctx, userID, chainType, walletAddress)
}

@ -0,0 +1,456 @@
package mgo
import (
"context"
"math/big"
"time"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/tools/errs"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// ---- RedPacket ----
type RedPacketMgo struct {
coll *mongo.Collection
}
func NewRedPacketMongo(db *mongo.Database) (database.RedPacket, error) {
coll := db.Collection("red_packet")
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
{
Keys: bson.D{{Key: "biz_id", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{{Key: "packet_id", Value: 1}},
},
{
Keys: bson.D{{Key: "group_id", Value: 1}},
},
})
if err != nil {
return nil, err
}
return &RedPacketMgo{coll: coll}, nil
}
func (m *RedPacketMgo) Create(ctx context.Context, rp *model.RedPacket) error {
_, err := m.coll.InsertOne(ctx, rp)
return err
}
func (m *RedPacketMgo) GetByBizID(ctx context.Context, bizID string) (*model.RedPacket, error) {
var rp model.RedPacket
err := m.coll.FindOne(ctx, bson.M{"biz_id": bizID}).Decode(&rp)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errs.ErrRecordNotFound.WrapMsg("red packet not found", "bizID", bizID)
}
return nil, err
}
return &rp, nil
}
func (m *RedPacketMgo) GetByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error) {
var rp model.RedPacket
err := m.coll.FindOne(ctx, bson.M{"packet_id": packetID}).Decode(&rp)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
}
return nil, err
}
return &rp, nil
}
func (m *RedPacketMgo) UpdateCreated(ctx context.Context, rp *model.RedPacket) error {
updates := bson.M{
"chain_type": rp.ChainType,
"packet_id": rp.PacketID,
"tx_hash": rp.TxHash,
"chain_id": rp.ChainID,
"contract_address": rp.ContractAddress,
"group_id": rp.GroupID,
"scope_type": rp.ScopeType,
"receiver_user_id": rp.ReceiverUserID,
"receiver_user_ids": rp.ReceiverUserIDs,
"status": rp.Status,
"updated_at": time.Now(),
}
res, err := m.coll.UpdateOne(ctx, bson.M{"biz_id": rp.BizID}, bson.M{"$set": updates})
if err != nil {
return err
}
if res.MatchedCount == 0 {
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "bizID", rp.BizID)
}
return nil
}
func (m *RedPacketMgo) UpdateStatus(ctx context.Context, packetID, status string) error {
res, err := m.coll.UpdateOne(ctx, bson.M{"packet_id": packetID},
bson.M{"$set": bson.M{"status": status, "updated_at": time.Now()}})
if err != nil {
return err
}
if res.MatchedCount == 0 {
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
}
return nil
}
func (m *RedPacketMgo) UpdateClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error {
var rp model.RedPacket
err := m.coll.FindOne(ctx, bson.M{"packet_id": packetID}).Decode(&rp)
if err != nil {
if err == mongo.ErrNoDocuments {
return errs.ErrRecordNotFound.WrapMsg("red packet not found", "packetID", packetID)
}
return err
}
totalClaimed := addNumericStrings(rp.ClaimedAmount, claimedAmount)
nextShares := rp.ClaimedShares + 1
updates := bson.M{
"claimed_amount": totalClaimed,
"claimed_shares": nextShares,
"updated_at": time.Now(),
}
if status != "" {
updates["status"] = status
}
_, err = m.coll.UpdateOne(ctx, bson.M{"packet_id": packetID}, bson.M{"$set": updates})
return err
}
func addNumericStrings(current, delta string) string {
left := new(big.Int)
if current != "" {
left.SetString(current, 10)
}
right := new(big.Int)
if delta != "" {
right.SetString(delta, 10)
}
return new(big.Int).Add(left, right).String()
}
// ---- RedPacketClaim ----
type RedPacketClaimMgo struct {
coll *mongo.Collection
}
func NewRedPacketClaimMongo(db *mongo.Database) (database.RedPacketClaim, error) {
coll := db.Collection("red_packet_claim")
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
{
Keys: bson.D{{Key: "claim_tx_hash", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "user_id", Value: 1}},
},
{
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "claimer_wallet", Value: 1}},
},
})
if err != nil {
return nil, err
}
return &RedPacketClaimMgo{coll: coll}, nil
}
func (m *RedPacketClaimMgo) Save(ctx context.Context, claim *model.RedPacketClaim) error {
if claim.UserID != "" {
var existing model.RedPacketClaim
err := m.coll.FindOne(ctx, bson.M{
"packet_id": claim.PacketID,
"user_id": claim.UserID,
}).Decode(&existing)
if err == nil {
updates := bson.M{
"claimer_wallet": claim.ClaimerWallet,
"auth_nonce": claim.AuthNonce,
"claim_tx_hash": claim.ClaimTxHash,
"claimed_amount": claim.ClaimedAmount,
"block_number": claim.BlockNumber,
"status": claim.Status,
"updated_at": claim.UpdatedAt,
}
_, err := m.coll.UpdateOne(ctx,
bson.M{"packet_id": claim.PacketID, "user_id": claim.UserID},
bson.M{"$set": updates})
return err
}
if err != mongo.ErrNoDocuments {
return err
}
}
_, err := m.coll.UpdateOne(ctx,
bson.M{"claim_tx_hash": claim.ClaimTxHash},
bson.M{"$set": claim},
options.Update().SetUpsert(true),
)
return err
}
func (m *RedPacketClaimMgo) GetByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error) {
var claim model.RedPacketClaim
err := m.coll.FindOne(ctx,
bson.M{"packet_id": packetID, "claimer_wallet": claimer},
options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}}),
).Decode(&claim)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errs.ErrRecordNotFound.WrapMsg("claim not found", "packetID", packetID, "claimer", claimer)
}
return nil, err
}
return &claim, nil
}
func (m *RedPacketClaimMgo) GetByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error) {
var claim model.RedPacketClaim
err := m.coll.FindOne(ctx,
bson.M{"packet_id": packetID, "user_id": userID},
options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}}),
).Decode(&claim)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errs.ErrRecordNotFound.WrapMsg("claim not found", "packetID", packetID, "userID", userID)
}
return nil, err
}
return &claim, nil
}
func (m *RedPacketClaimMgo) ListByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error) {
cursor, err := m.coll.Find(ctx,
bson.M{"packet_id": packetID},
options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}),
)
if err != nil {
return nil, err
}
var claims []*model.RedPacketClaim
if err := cursor.All(ctx, &claims); err != nil {
return nil, err
}
return claims, nil
}
// ---- RedPacketClaimAuth ----
type RedPacketClaimAuthMgo struct {
coll *mongo.Collection
}
func NewRedPacketClaimAuthMongo(db *mongo.Database) (database.RedPacketClaimAuth, error) {
coll := db.Collection("red_packet_claim_auth")
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
{
Keys: bson.D{{Key: "auth_nonce", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{{Key: "packet_id", Value: 1}, {Key: "claimer", Value: 1}},
},
})
if err != nil {
return nil, err
}
return &RedPacketClaimAuthMgo{coll: coll}, nil
}
func (m *RedPacketClaimAuthMgo) Create(ctx context.Context, auth *model.RedPacketClaimAuth) error {
_, err := m.coll.InsertOne(ctx, auth)
return err
}
func (m *RedPacketClaimAuthMgo) Get(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error) {
var auth model.RedPacketClaimAuth
err := m.coll.FindOne(ctx, bson.M{
"packet_id": packetID,
"claimer": claimer,
"used": false,
}).Decode(&auth)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errs.ErrRecordNotFound.WrapMsg("claim auth not found", "packetID", packetID, "claimer", claimer)
}
return nil, err
}
return &auth, nil
}
func (m *RedPacketClaimAuthMgo) MarkUsed(ctx context.Context, authNonce string) error {
res, err := m.coll.UpdateOne(ctx,
bson.M{"auth_nonce": authNonce},
bson.M{"$set": bson.M{"used": true}},
)
if err != nil {
return err
}
if res.MatchedCount == 0 {
return errs.ErrRecordNotFound.WrapMsg("claim auth not found", "authNonce", authNonce)
}
return nil
}
// ---- RedPacketRefund ----
type RedPacketRefundMgo struct {
coll *mongo.Collection
}
func NewRedPacketRefundMongo(db *mongo.Database) (database.RedPacketRefund, error) {
coll := db.Collection("red_packet_refund")
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
Keys: bson.D{{Key: "tx_hash", Value: 1}},
Options: options.Index().SetUnique(true),
})
if err != nil {
return nil, err
}
return &RedPacketRefundMgo{coll: coll}, nil
}
func (m *RedPacketRefundMgo) Save(ctx context.Context, refund *model.RedPacketRefund) error {
_, err := m.coll.UpdateOne(ctx,
bson.M{"tx_hash": refund.TxHash},
bson.M{"$setOnInsert": refund},
options.Update().SetUpsert(true),
)
return err
}
// ---- WalletBindingChallenge ----
type WalletBindingChallengeMgo struct {
coll *mongo.Collection
}
func NewWalletBindingChallengeMongo(db *mongo.Database) (database.WalletBindingChallenge, error) {
coll := db.Collection("wallet_binding_challenge")
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
{
Keys: bson.D{{Key: "challenge_id", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{{Key: "user_id", Value: 1}},
},
{
Keys: bson.D{{Key: "wallet_address", Value: 1}},
},
})
if err != nil {
return nil, err
}
return &WalletBindingChallengeMgo{coll: coll}, nil
}
func (m *WalletBindingChallengeMgo) Create(ctx context.Context, challenge *model.WalletBindingChallenge) error {
_, err := m.coll.InsertOne(ctx, challenge)
return err
}
func (m *WalletBindingChallengeMgo) Get(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error) {
var c model.WalletBindingChallenge
err := m.coll.FindOne(ctx, bson.M{"challenge_id": challengeID}).Decode(&c)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errs.ErrRecordNotFound.WrapMsg("wallet binding challenge not found", "challengeID", challengeID)
}
return nil, err
}
return &c, nil
}
func (m *WalletBindingChallengeMgo) Update(ctx context.Context, c *model.WalletBindingChallenge) error {
updates := bson.M{
"status": c.Status,
"signature": c.Signature,
"verified_at": c.VerifiedAt,
"updated_at": c.UpdatedAt,
}
res, err := m.coll.UpdateOne(ctx, bson.M{"challenge_id": c.ChallengeID}, bson.M{"$set": updates})
if err != nil {
return err
}
if res.MatchedCount == 0 {
return errs.ErrRecordNotFound.WrapMsg("wallet binding challenge not found", "challengeID", c.ChallengeID)
}
return nil
}
// ---- WalletBinding ----
type WalletBindingMgo struct {
coll *mongo.Collection
}
func NewWalletBindingMongo(db *mongo.Database) (database.WalletBinding, error) {
coll := db.Collection("wallet_binding")
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
{
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "chain_type", Value: 1}, {Key: "wallet_address", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{{Key: "user_id", Value: 1}},
},
})
if err != nil {
return nil, err
}
return &WalletBindingMgo{coll: coll}, nil
}
func (m *WalletBindingMgo) Upsert(ctx context.Context, b *model.WalletBinding) error {
filter := bson.M{
"user_id": b.UserID,
"chain_type": b.ChainType,
"wallet_address": b.WalletAddress,
}
updates := bson.M{
"chain_id": b.ChainID,
"status": b.Status,
"challenge_id": b.ChallengeID,
"verified_at": b.VerifiedAt,
"revoked_at": b.RevokedAt,
"updated_at": b.UpdatedAt,
}
setOnInsert := bson.M{
"created_at": b.CreatedAt,
}
_, err := m.coll.UpdateOne(ctx, filter,
bson.M{"$set": updates, "$setOnInsert": setOnInsert},
options.Update().SetUpsert(true),
)
return err
}
func (m *WalletBindingMgo) GetActive(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error) {
var b model.WalletBinding
err := m.coll.FindOne(ctx, bson.M{
"user_id": userID,
"chain_type": chainType,
"wallet_address": walletAddress,
"status": "ACTIVE",
}).Decode(&b)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errs.ErrRecordNotFound.WrapMsg("active wallet binding not found", "userID", userID, "chainType", chainType, "walletAddress", walletAddress)
}
return nil, err
}
return &b, nil
}

@ -0,0 +1,44 @@
package database
import (
"context"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
)
type RedPacket interface {
Create(ctx context.Context, rp *model.RedPacket) error
GetByBizID(ctx context.Context, bizID string) (*model.RedPacket, error)
GetByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error)
UpdateCreated(ctx context.Context, rp *model.RedPacket) error
UpdateStatus(ctx context.Context, packetID, status string) error
UpdateClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error
}
type RedPacketClaim interface {
Save(ctx context.Context, claim *model.RedPacketClaim) error
GetByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error)
GetByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error)
ListByPacketID(ctx context.Context, packetID string) ([]*model.RedPacketClaim, error)
}
type RedPacketClaimAuth interface {
Create(ctx context.Context, auth *model.RedPacketClaimAuth) error
Get(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error)
MarkUsed(ctx context.Context, authNonce string) error
}
type RedPacketRefund interface {
Save(ctx context.Context, refund *model.RedPacketRefund) error
}
type WalletBindingChallenge interface {
Create(ctx context.Context, challenge *model.WalletBindingChallenge) error
Get(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error)
Update(ctx context.Context, challenge *model.WalletBindingChallenge) error
}
type WalletBinding interface {
Upsert(ctx context.Context, binding *model.WalletBinding) error
GetActive(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error)
}

@ -0,0 +1,91 @@
package model
import "time"
type RedPacket struct {
BizID string `bson:"biz_id"`
ChainType string `bson:"chain_type"`
PacketID string `bson:"packet_id"`
ChainID int64 `bson:"chain_id"`
ContractAddress string `bson:"contract_address"`
CreatorUserID string `bson:"creator_user_id"`
CreatorWallet string `bson:"creator_wallet"`
GroupID string `bson:"group_id"`
ScopeType string `bson:"scope_type"`
ReceiverUserID string `bson:"receiver_user_id"`
ReceiverUserIDs []string `bson:"receiver_user_ids"`
PacketType int32 `bson:"packet_type"`
Token string `bson:"token"`
TotalAmount string `bson:"total_amount"`
TotalShares int32 `bson:"total_shares"`
ClaimedAmount string `bson:"claimed_amount"`
ClaimedShares int32 `bson:"claimed_shares"`
ExpiryAt int64 `bson:"expiry_at"`
TxHash string `bson:"tx_hash"`
Status string `bson:"status"`
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}
type RedPacketClaim struct {
PacketID string `bson:"packet_id"`
UserID string `bson:"user_id"`
ClaimerWallet string `bson:"claimer_wallet"`
AuthNonce string `bson:"auth_nonce"`
ClaimTxHash string `bson:"claim_tx_hash"`
ClaimedAmount string `bson:"claimed_amount"`
BlockNumber uint64 `bson:"block_number"`
Status string `bson:"status"`
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}
type RedPacketClaimAuth struct {
PacketID string `bson:"packet_id"`
Claimer string `bson:"claimer"`
AuthNonce string `bson:"auth_nonce"`
RandomSeed string `bson:"random_seed"`
Deadline int64 `bson:"deadline"`
Signature string `bson:"signature"`
Used bool `bson:"used"`
CreatedAt time.Time `bson:"created_at"`
}
type RedPacketRefund struct {
PacketID string `bson:"packet_id"`
RefundTo string `bson:"refund_to"`
TxHash string `bson:"tx_hash"`
Amount string `bson:"amount"`
CreatedAt time.Time `bson:"created_at"`
}
type WalletBindingChallenge struct {
ChallengeID string `bson:"challenge_id"`
UserID string `bson:"user_id"`
ChainType string `bson:"chain_type"`
ChainID int64 `bson:"chain_id"`
WalletAddress string `bson:"wallet_address"`
Nonce string `bson:"nonce"`
Message string `bson:"message"`
Protocol string `bson:"protocol"`
SignMethod string `bson:"sign_method"`
Status string `bson:"status"`
Signature string `bson:"signature"`
ExpiresAt time.Time `bson:"expires_at"`
VerifiedAt *time.Time `bson:"verified_at,omitempty"`
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}
type WalletBinding struct {
UserID string `bson:"user_id"`
ChainType string `bson:"chain_type"`
ChainID int64 `bson:"chain_id"`
WalletAddress string `bson:"wallet_address"`
Status string `bson:"status"`
ChallengeID string `bson:"challenge_id"`
VerifiedAt time.Time `bson:"verified_at"`
RevokedAt *time.Time `bson:"revoked_at,omitempty"`
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}

@ -0,0 +1,14 @@
package rpcli
import (
pbredpacket "github.com/openimsdk/protocol/redpacket"
"google.golang.org/grpc"
)
func NewRedPacketClient(cc grpc.ClientConnInterface) *RedPacketClient {
return &RedPacketClient{pbredpacket.NewRedPacketClient(cc)}
}
type RedPacketClient struct {
pbredpacket.RedPacketClient
}

@ -1 +1 @@
Subproject commit 90aae1d576466a1fa55eba386d1f7a38ca6062d0
Subproject commit 9f69daaff1f7b46b971bb7b97cd993cd6302b41e
Loading…
Cancel
Save