From 541471f401f4f1660e082546fce703b0341451d0 Mon Sep 17 00:00:00 2001 From: hawklin2017 <32898629+hawklin2017@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:11:06 +0800 Subject: [PATCH] redpacket --- cmd/openim-rpc/openim-rpc-redpacket/README.md | 176 ++- .../openim-rpc-redpacket/config/config.go | 71 -- .../openim-rpc-redpacket/config/config.yaml | 23 - cmd/openim-rpc/openim-rpc-redpacket/go.mod | 68 -- cmd/openim-rpc/openim-rpc-redpacket/go.sum | 260 ---- .../internal/authctx/user.go | 49 - .../internal/handler/admin.go | 134 --- .../internal/handler/redpacket.go | 176 --- .../internal/model/model.go | 99 -- .../internal/repository/repo.go | 251 ---- .../internal/service/admin.go | 138 --- .../internal/service/redpacket.go | 1068 ----------------- .../internal/service/redpacket_test.go | 386 ------ cmd/openim-rpc/openim-rpc-redpacket/main.go | 159 +-- .../openim-rpc-redpacket/pkg/resp/resp.go | 40 - .../openim-rpc-redpacket/redpacket.db | Bin 49152 -> 0 bytes .../openim-rpc-redpacket/router/router.go | 37 - config/openim-rpc-redpacket.yml | 31 + config/share.yml | 1 + go.mod | 22 +- go.sum | 94 +- internal/api/captcha.go | 2 +- internal/api/init.go | 1 + internal/api/redpacket.go | 217 ++++ internal/api/router.go | 27 + internal/rpc/captcha/captcha.go | 13 +- internal/rpc/redpacket/admin.go | 142 +++ .../rpc/redpacket}/chain/abi/RedPacket.json | 0 .../rpc/redpacket}/chain/client.go | 20 +- .../rpc/redpacket}/chain/indexer.go | 64 +- .../rpc/redpacket}/chain/parser.go | 8 - .../rpc/redpacket}/chain/parser_test.go | 0 .../rpc/redpacket}/chain/tron.go | 24 +- .../rpc/redpacket}/chain/tron_indexer.go | 73 +- .../rpc/redpacket}/chain/tron_test.go | 0 internal/rpc/redpacket/redpacket.go | 132 ++ internal/rpc/redpacket/service.go | 777 ++++++++++++ internal/rpc/redpacket/wallet.go | 251 ++++ pkg/common/cmd/constant.go | 5 +- pkg/common/cmd/rpc_redpacket.go | 47 + pkg/common/config/config.go | 40 + pkg/common/storage/controller/redpacket.go | 141 +++ pkg/common/storage/database/mgo/redpacket.go | 456 +++++++ pkg/common/storage/database/redpacket.go | 44 + pkg/common/storage/model/redpacket.go | 91 ++ pkg/rpcli/redpacket.go | 14 + protocol | 2 +- 47 files changed, 2684 insertions(+), 3190 deletions(-) delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/config/config.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/config/config.yaml delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/go.mod delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/go.sum delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/internal/authctx/user.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/internal/handler/admin.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/internal/handler/redpacket.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/internal/model/model.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/internal/repository/repo.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/internal/service/admin.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket_test.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/pkg/resp/resp.go delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/redpacket.db delete mode 100644 cmd/openim-rpc/openim-rpc-redpacket/router/router.go create mode 100644 config/openim-rpc-redpacket.yml create mode 100644 internal/api/redpacket.go create mode 100644 internal/rpc/redpacket/admin.go rename {cmd/openim-rpc/openim-rpc-redpacket/internal => internal/rpc/redpacket}/chain/abi/RedPacket.json (100%) rename {cmd/openim-rpc/openim-rpc-redpacket/internal => internal/rpc/redpacket}/chain/client.go (88%) rename {cmd/openim-rpc/openim-rpc-redpacket/internal => internal/rpc/redpacket}/chain/indexer.go (62%) rename {cmd/openim-rpc/openim-rpc-redpacket/internal => internal/rpc/redpacket}/chain/parser.go (85%) rename {cmd/openim-rpc/openim-rpc-redpacket/internal => internal/rpc/redpacket}/chain/parser_test.go (100%) rename {cmd/openim-rpc/openim-rpc-redpacket/internal => internal/rpc/redpacket}/chain/tron.go (91%) rename {cmd/openim-rpc/openim-rpc-redpacket/internal => internal/rpc/redpacket}/chain/tron_indexer.go (64%) rename {cmd/openim-rpc/openim-rpc-redpacket/internal => internal/rpc/redpacket}/chain/tron_test.go (100%) create mode 100644 internal/rpc/redpacket/redpacket.go create mode 100644 internal/rpc/redpacket/service.go create mode 100644 internal/rpc/redpacket/wallet.go create mode 100644 pkg/common/cmd/rpc_redpacket.go create mode 100644 pkg/common/storage/controller/redpacket.go create mode 100644 pkg/common/storage/database/mgo/redpacket.go create mode 100644 pkg/common/storage/database/redpacket.go create mode 100644 pkg/common/storage/model/redpacket.go create mode 100644 pkg/rpcli/redpacket.go diff --git a/cmd/openim-rpc/openim-rpc-redpacket/README.md b/cmd/openim-rpc/openim-rpc-redpacket/README.md index 894c29d90..c22d6f9f4 100644 --- a/cmd/openim-rpc/openim-rpc-redpacket/README.md +++ b/cmd/openim-rpc/openim-rpc-redpacket/README.md @@ -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. diff --git a/cmd/openim-rpc/openim-rpc-redpacket/config/config.go b/cmd/openim-rpc/openim-rpc-redpacket/config/config.go deleted file mode 100644 index 288ce3d71..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/config/config.go +++ /dev/null @@ -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 -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/config/config.yaml b/cmd/openim-rpc/openim-rpc-redpacket/config/config.yaml deleted file mode 100644 index fed6d2f91..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/config/config.yaml +++ /dev/null @@ -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 diff --git a/cmd/openim-rpc/openim-rpc-redpacket/go.mod b/cmd/openim-rpc/openim-rpc-redpacket/go.mod deleted file mode 100644 index b0f786f13..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/go.mod +++ /dev/null @@ -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 -) diff --git a/cmd/openim-rpc/openim-rpc-redpacket/go.sum b/cmd/openim-rpc/openim-rpc-redpacket/go.sum deleted file mode 100644 index 96ff7bbb7..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/go.sum +++ /dev/null @@ -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= diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/authctx/user.go b/cmd/openim-rpc/openim-rpc-redpacket/internal/authctx/user.go deleted file mode 100644 index bd277b742..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/authctx/user.go +++ /dev/null @@ -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 -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/handler/admin.go b/cmd/openim-rpc/openim-rpc-redpacket/internal/handler/admin.go deleted file mode 100644 index 607adb0bd..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/handler/admin.go +++ /dev/null @@ -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) -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/handler/redpacket.go b/cmd/openim-rpc/openim-rpc-redpacket/internal/handler/redpacket.go deleted file mode 100644 index b8cb18bf7..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/handler/redpacket.go +++ /dev/null @@ -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) -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/model/model.go b/cmd/openim-rpc/openim-rpc-redpacket/internal/model/model.go deleted file mode 100644 index b3e63b423..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/model/model.go +++ /dev/null @@ -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"` -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/repository/repo.go b/cmd/openim-rpc/openim-rpc-redpacket/internal/repository/repo.go deleted file mode 100644 index 4baf10864..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/repository/repo.go +++ /dev/null @@ -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() -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/service/admin.go b/cmd/openim-rpc/openim-rpc-redpacket/internal/service/admin.go deleted file mode 100644 index 5ddc98a39..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/service/admin.go +++ /dev/null @@ -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) -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket.go b/cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket.go deleted file mode 100644 index 65c8035e2..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket.go +++ /dev/null @@ -1,1068 +0,0 @@ -package service - -import ( - "context" - "crypto/ecdsa" - "encoding/hex" - "encoding/json" - "fmt" - "math/big" - "strings" - "time" - - "redpacket/internal/authctx" - "redpacket/internal/chain" - "redpacket/internal/model" - "redpacket/internal/repository" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/google/uuid" - "gorm.io/gorm" -) - -type RedPacketService struct { - repo repository.Repository - chainClient *chain.ChainClient - tronClient *chain.TronClient - signerKey *ecdsa.PrivateKey -} - -type CreateOrderRequest struct { - ChainType string `json:"chain_type" binding:"required"` - ChainID int64 `json:"chain_id"` - ContractAddress string `json:"contract_address"` - CreatorUserID string `json:"creator_user_id"` - CreatorWallet string `json:"creator_wallet" binding:"required"` - GroupID string `json:"group_id"` - ScopeType string `json:"scope_type"` - ReceiverUserID string `json:"receiver_user_id"` - ReceiverUserIDs []string `json:"receiver_user_ids"` - PacketType int32 `json:"packet_type" binding:"required"` - Token string `json:"token"` - TotalAmount string `json:"total_amount" binding:"required"` - TotalShares int32 `json:"total_shares" binding:"required"` - ExpiryAt int64 `json:"expiry_at"` - Remark string `json:"remark"` -} - -type CreatedCallbackRequest struct { - BizID string `json:"biz_id" binding:"required"` - TxHash string `json:"tx_hash" binding:"required"` - PacketID string `json:"packet_id"` - GroupID string `json:"group_id"` - ScopeType string `json:"scope_type"` - ReceiverUserID string `json:"receiver_user_id"` - ReceiverUserIDs []string `json:"receiver_user_ids"` -} - -type ClaimResultRequest struct { - PacketID string `json:"packet_id" binding:"required"` - Claimer string `json:"claimer" binding:"required"` - UserID string `json:"-"` - TxHash string `json:"tx_hash" binding:"required"` -} - -type WalletBindChallengeRequest struct { - UserID string `json:"user_id"` - ChainType string `json:"chain_type" binding:"required"` - ChainID int64 `json:"chain_id"` - WalletAddress string `json:"wallet_address" binding:"required"` - Domain string `json:"domain"` - URI string `json:"uri"` -} - -type WalletBindConfirmRequest struct { - ChallengeID string `json:"challenge_id" binding:"required"` - Signature string `json:"signature" binding:"required"` -} - -func NewRedPacketService(repo repository.Repository, chainClient *chain.ChainClient, tronClient *chain.TronClient, signerPrivateKey string) *RedPacketService { - var signerKey *ecdsa.PrivateKey - if signerPrivateKey != "" { - var err error - signerKey, err = crypto.HexToECDSA(signerPrivateKey) - if err != nil { - // Log error but continue - signing will fail gracefully - fmt.Printf("Warning: failed to parse signer private key: %v\n", err) - } - } - - return &RedPacketService{ - repo: repo, - chainClient: chainClient, - tronClient: tronClient, - signerKey: signerKey, - } -} - -func (s *RedPacketService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (map[string]interface{}, error) { - currentUserID, err := authctx.CurrentUserID(ctx) - if err != nil { - return nil, err - } - req.CreatorUserID = currentUserID - - 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 - } - - receiverUserIDs, err := encodeReceiverUserIDs(req.ReceiverUserIDs) - if err != nil { - return nil, fmt.Errorf("encode receiver_user_ids failed: %w", 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: req.CreatorUserID, - CreatorWallet: req.CreatorWallet, - GroupID: req.GroupID, - ScopeType: scopeType, - ReceiverUserID: req.ReceiverUserID, - ReceiverUserIDs: 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.repo.CreateRedPacket(ctx, rp); err != nil { - return nil, fmt.Errorf("failed to create red packet: %w", err) - } - - return map[string]interface{}{ - "biz_id": bizID, - }, nil -} - -func (s *RedPacketService) CreatedCallback(ctx context.Context, req *CreatedCallbackRequest) error { - rp, err := s.repo.GetRedPacketByBizID(ctx, req.BizID) - if err != nil { - return fmt.Errorf("biz record not found: %s", req.BizID) - } - - 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, err = encodeReceiverUserIDs(req.ReceiverUserIDs) - if err != nil { - return fmt.Errorf("encode receiver_user_ids failed: %w", err) - } - } - - if err := validateCreateScope(scopeType, groupID, receiverUserID, decodeReceiverUserIDs(receiverUserIDs)); err != nil { - return err - } - - createdPacket, err := s.resolveCreatedPacket(ctx, rp, req.TxHash, req.PacketID) - if err != nil { - return err - } - - return s.repo.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", - }) -} - -func (s *RedPacketService) GetDetail(ctx context.Context, packetID string) (map[string]interface{}, error) { - rp, err := s.repo.GetRedPacketByPacketID(ctx, packetID) - if err != nil { - return nil, fmt.Errorf("packet not found: %s", packetID) - } - - claims, err := s.repo.GetClaimsByPacketID(ctx, packetID) - if err != nil { - claims = []model.RedPacketClaim{} - } - - return map[string]interface{}{ - "biz_record": rp, - "claims": claims, - }, nil -} - -func (s *RedPacketService) CanClaim(ctx context.Context, packetID, claimer, userID string) error { - rp, err := s.repo.GetRedPacketByPacketID(ctx, packetID) - if err != nil { - return fmt.Errorf("packet not found: %s", packetID) - } - - 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 fmt.Errorf("unsupported packet_type: %d", rp.PacketType) - } -} - -// SignClaim generates signature for claim operation -func (s *RedPacketService) IssueClaimSign(ctx context.Context, packetID, claimer, randomSeed string) (map[string]interface{}, error) { - userID, err := authctx.CurrentUserID(ctx) - if err != nil { - return nil, err - } - if err := s.CanClaim(ctx, packetID, claimer, userID); err != nil { - return nil, err - } - - packetIDBig := new(big.Int) - if _, ok := packetIDBig.SetString(packetID, 10); !ok { - return nil, fmt.Errorf("invalid packet_id: %s", packetID) - } - - claimerAddr := common.HexToAddress(claimer) - - // Generate nonce and deadline (5 minute expiry) - nonce := fmt.Sprintf("%d", time.Now().UnixNano()) - authNonceBig := new(big.Int) - if _, ok := authNonceBig.SetString(nonce, 10); !ok { - return nil, fmt.Errorf("invalid auth nonce") - } - deadline := time.Now().Add(5 * time.Minute).Unix() - randomSeedBig := new(big.Int) - if randomSeed != "" && randomSeed != "0" { - if _, ok := randomSeedBig.SetString(randomSeed, 10); !ok { - return nil, fmt.Errorf("invalid random_seed: %s", randomSeed) - } - } else { - randomSeedBig.SetInt64(time.Now().UnixNano()) - } - deadlineBig := big.NewInt(deadline) - - var digest [32]byte - - if s.chainClient != nil { - // Use real contract call to getSignMessage - digest, err = s.chainClient.GetSignMessage(ctx, packetIDBig, claimerAddr, authNonceBig, randomSeedBig, deadlineBig) - if err != nil { - return nil, fmt.Errorf("getSignMessage failed: %w", err) - } - } else { - // Fallback for testing - digest = crypto.Keccak256Hash([]byte(fmt.Sprintf("%s:%s:%s:%s:%d", packetID, claimer, nonce, randomSeedBig.String(), deadline))) - } - - // Sign the digest - var signature []byte - if s.signerKey != nil { - signature, err = crypto.Sign(digest[:], s.signerKey) - if err != nil { - return nil, fmt.Errorf("sign failed: %w", err) - } - if len(signature) == 65 && signature[64] < 27 { - signature[64] += 27 - } - } else { - signature = []byte("0xplaceholder-signature-for-testing") - } - - sigHex := "0x" + hex.EncodeToString(signature) - - auth := &model.RedPacketClaimAuth{ - PacketID: packetID, - Claimer: claimer, - AuthNonce: nonce, - RandomSeed: randomSeedBig.String(), - Deadline: deadline, - Signature: sigHex, - CreatedAt: time.Now(), - } - - if err := s.repo.CreateClaimAuth(ctx, auth); err != nil { - return nil, fmt.Errorf("save claim auth failed: %w", err) - } - - return map[string]interface{}{ - "auth_nonce": nonce, - "deadline": deadline, - "signature": sigHex, - "random_seed": randomSeedBig.String(), - }, nil -} - -func (s *RedPacketService) ClaimResult(ctx context.Context, req *ClaimResultRequest) error { - userID, err := authctx.CurrentUserID(ctx) - if err != nil { - return err - } - req.UserID = userID - - rp, err := s.repo.GetRedPacketByPacketID(ctx, req.PacketID) - if err != nil { - return fmt.Errorf("packet not found: %s", req.PacketID) - } - - if err := validateClaimBase(rp, req.UserID, req.Claimer); err != nil { - return err - } - - claim := &model.RedPacketClaim{ - PacketID: req.PacketID, - UserID: req.UserID, - ClaimerWallet: req.Claimer, - ClaimTxHash: req.TxHash, - Status: "PENDING", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - if err := s.repo.SaveClaim(ctx, claim); err != nil { - return err - } - - claimedEvent, err := s.resolveClaimedEvent(ctx, rp, req.TxHash) - if err != nil { - return nil - } - if claimedEvent == nil { - return nil - } - if !strings.EqualFold(claimedEvent.ClaimerWallet, req.Claimer) { - return fmt.Errorf("claim event claimer mismatch: got %s want %s", claimedEvent.ClaimerWallet, req.Claimer) - } - - confirmed := &model.RedPacketClaim{ - PacketID: req.PacketID, - UserID: req.UserID, - 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.repo.SaveClaim(ctx, confirmed); err != nil { - return err - } - - if claimedEvent.AuthNonce != "" { - if err := s.repo.MarkClaimAuthUsed(ctx, claimedEvent.AuthNonce); err != nil { - return err - } - } - - nextStatus := derivePacketStatusAfterClaim(rp, claimedEvent.Amount) - return s.repo.UpdateRedPacketClaimProgress(ctx, req.PacketID, claimedEvent.Amount, nextStatus) -} - -func (s *RedPacketService) IssueWalletBindChallenge(ctx context.Context, req *WalletBindChallengeRequest) (map[string]interface{}, error) { - currentUserID, err := authctx.CurrentUserID(ctx) - if err != nil { - return nil, err - } - req.UserID = currentUserID - - chainType, err := normalizeChainType(req.ChainType) - if err != nil { - return nil, err - } - - walletAddress := strings.TrimSpace(req.WalletAddress) - if walletAddress == "" { - return nil, fmt.Errorf("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(req, challengeID, nonce, issuedAt, expiresAt) - if chainType == "TRON" { - protocol = "tron-signmessagev2" - signMethod = "signMessageV2" - message = buildTRONBindMessage(req, challengeID, nonce, issuedAt, expiresAt) - } - - challenge := &model.WalletBindingChallenge{ - ChallengeID: challengeID, - UserID: req.UserID, - 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.repo.CreateWalletBindingChallenge(ctx, challenge); err != nil { - return nil, err - } - - return map[string]interface{}{ - "challenge_id": challengeID, - "user_id": req.UserID, - "chain_type": chainType, - "chain_id": req.ChainID, - "wallet": walletAddress, - "protocol": protocol, - "sign_method": signMethod, - "nonce": nonce, - "message": message, - "issued_at": issuedAt.Format(time.RFC3339), - "expires_at": expiresAt.Format(time.RFC3339), - }, nil -} - -func (s *RedPacketService) ConfirmWalletBind(ctx context.Context, req *WalletBindConfirmRequest) (map[string]interface{}, error) { - challenge, err := s.repo.GetWalletBindingChallenge(ctx, req.ChallengeID) - if err != nil { - return nil, fmt.Errorf("challenge not found: %s", req.ChallengeID) - } - if challenge.Status != "PENDING" { - return nil, fmt.Errorf("challenge is not pending") - } - if time.Now().UTC().After(challenge.ExpiresAt) { - challenge.Status = "EXPIRED" - challenge.UpdatedAt = time.Now() - _ = s.repo.UpdateWalletBindingChallenge(ctx, challenge) - return nil, fmt.Errorf("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.repo.UpdateWalletBindingChallenge(ctx, challenge) - return nil, err - } - case "TRON": - return nil, fmt.Errorf("TRON wallet binding verification is not implemented yet") - default: - return nil, fmt.Errorf("unsupported chain_type: %s", challenge.ChainType) - } - - now := time.Now().UTC() - challenge.Status = "VERIFIED" - challenge.Signature = req.Signature - challenge.VerifiedAt = &now - challenge.UpdatedAt = now - if err := s.repo.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.repo.UpsertWalletBinding(ctx, binding); err != nil { - return nil, err - } - - return map[string]interface{}{ - "user_id": binding.UserID, - "chain_type": binding.ChainType, - "chain_id": binding.ChainID, - "wallet_address": binding.WalletAddress, - "status": binding.Status, - "verified_at": binding.VerifiedAt.Format(time.RFC3339), - }, nil -} - -func (s *RedPacketService) GetWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (map[string]interface{}, error) { - currentUserID, err := authctx.CurrentUserID(ctx) - if err != nil { - return nil, err - } - userID = currentUserID - - normalizedChainType, err := normalizeChainType(chainType) - if err != nil { - return nil, err - } - binding, err := s.repo.GetActiveWalletBinding(ctx, userID, normalizedChainType, walletAddress) - if err != nil { - return nil, fmt.Errorf("active wallet binding not found") - } - return map[string]interface{}{ - "user_id": binding.UserID, - "chain_type": binding.ChainType, - "chain_id": binding.ChainID, - "wallet_address": binding.WalletAddress, - "status": binding.Status, - "challenge_id": binding.ChallengeID, - "verified_at": binding.VerifiedAt.Format(time.RFC3339), - }, nil -} - -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 *RedPacketService) 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, fmt.Errorf("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, fmt.Errorf("parse created tx failed: %w", err) - } - 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, fmt.Errorf("PacketCreated event not found in tx: %s", txHashHex) - } - return buildFallbackCreatedPacket(rp, fallbackPacketID), nil - case "TRON": - if s.tronClient == nil { - if fallbackPacketID == "" { - return nil, fmt.Errorf("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, fmt.Errorf("parse tron created tx failed: %w", err) - } - 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, fmt.Errorf("PacketCreated event not found in TRON tx: %s", txHashHex) - } - return buildFallbackCreatedPacket(rp, fallbackPacketID), nil - default: - return nil, fmt.Errorf("unsupported chain_type: %s", rp.ChainType) - } -} - -// validateCreateHook reserves a centralized validation extension point for -// create-order. Concrete centralized checks are deferred, but validation is -// already split by packet type so future rules can evolve independently. -func (s *RedPacketService) validateCreateHook(ctx context.Context, req *CreateOrderRequest) 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 fmt.Errorf("unsupported packet_type: %d", req.PacketType) - } -} - -// validateFixedPacketCreate reserves centralized checks for fixed red packets. -// todo: validate creator identity, group validity, and group membership. -func (s *RedPacketService) validateFixedPacketCreate(ctx context.Context, req *CreateOrderRequest) error { - return nil -} - -// validateRandomPacketCreate reserves centralized checks for random red packets. -// todo: validate creator identity, group validity, and group membership. -func (s *RedPacketService) validateRandomPacketCreate(ctx context.Context, req *CreateOrderRequest) error { - return nil -} - -// validateTransferPacketCreate reserves centralized checks for transfer packets. -// todo: validate creator identity and sender/receiver relationship. -func (s *RedPacketService) validateTransferPacketCreate(ctx context.Context, req *CreateOrderRequest) 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 fmt.Errorf("created packet is nil") - } - - if createdPacket.CreatorWallet != "" && strings.ToLower(rp.CreatorWallet) != createdPacket.CreatorWallet { - return fmt.Errorf("creator mismatch: got %s want %s", createdPacket.CreatorWallet, rp.CreatorWallet) - } - if createdPacket.PacketType != rp.PacketType { - return fmt.Errorf("packet type mismatch: got %d want %d", createdPacket.PacketType, rp.PacketType) - } - if createdPacket.TotalAmount != rp.TotalAmount { - return fmt.Errorf("total amount mismatch: got %s want %s", createdPacket.TotalAmount, rp.TotalAmount) - } - if createdPacket.TotalShares != rp.TotalShares { - return fmt.Errorf("total shares mismatch: got %d want %d", createdPacket.TotalShares, rp.TotalShares) - } - expectedToken := normalizeTokenAddress(rp.Token) - if createdPacket.Token != expectedToken { - return fmt.Errorf("token mismatch: got %s want %s", createdPacket.Token, expectedToken) - } - if rp.ExpiryAt > 0 && createdPacket.ExpiryAt != rp.ExpiryAt { - return fmt.Errorf("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 fmt.Errorf("packet not found") - } - if strings.TrimSpace(userID) == "" { - return fmt.Errorf("user_id is required") - } - if strings.TrimSpace(claimer) == "" { - return fmt.Errorf("claimer is required") - } - if rp.Status != "ACTIVE" { - return fmt.Errorf("packet is not active, current status: %s", rp.Status) - } - if rp.ExpiryAt > 0 && rp.ExpiryAt <= time.Now().Unix() { - return fmt.Errorf("packet is expired") - } - if rp.Status == "REFUNDED" { - return fmt.Errorf("packet is refunded") - } - return nil -} - -func (s *RedPacketService) validateFixedPacketClaim(ctx context.Context, rp *model.RedPacket, userID, claimer string) error { - if strings.TrimSpace(rp.GroupID) == "" { - return fmt.Errorf("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 *RedPacketService) validateRandomPacketClaim(ctx context.Context, rp *model.RedPacket, userID, claimer string) error { - if strings.TrimSpace(rp.GroupID) == "" { - return fmt.Errorf("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 *RedPacketService) 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 fmt.Errorf("receiver_user_id is required for transfer claim") - } - if rp.ReceiverUserID != userID { - return fmt.Errorf("user is not the designated receiver") - } - return s.ensureFriendRelationship(ctx, rp.CreatorUserID, userID) -} - -func (s *RedPacketService) ensureNotClaimed(ctx context.Context, packetID, userID, claimer string) error { - if strings.TrimSpace(userID) != "" { - claim, err := s.repo.GetClaimByPacketIDAndUserID(ctx, packetID, userID) - if err == nil && claim != nil && claim.Status != "FAILED" { - return fmt.Errorf("user already claimed") - } - if err != nil && err != gorm.ErrRecordNotFound { - return fmt.Errorf("failed to check user claim status: %w", err) - } - } - - claim, err := s.repo.GetClaimByPacketIDAndClaimer(ctx, packetID, claimer) - if err == nil && claim != nil && claim.Status != "FAILED" { - return fmt.Errorf("already claimed") - } - if err != nil && err != gorm.ErrRecordNotFound { - return fmt.Errorf("failed to check claim status: %w", err) - } - return nil -} - -// ensureWalletBinding reserves the centralized identity check between Web2 -// user identity and wallet address used for claiming. -func (s *RedPacketService) ensureWalletBinding(ctx context.Context, userID, claimer, chainType string) error { - if _, err := s.repo.GetActiveWalletBinding(ctx, userID, chainType, claimer); err != nil { - if err == gorm.ErrRecordNotFound { - return fmt.Errorf("wallet is not bound to user") - } - return fmt.Errorf("check wallet binding failed: %w", err) - } - return nil -} - -// ensureGroupEligibility reserves centralized group validation, including -// whether the group exists and whether the user is currently a member. -func (s *RedPacketService) ensureGroupEligibility(ctx context.Context, groupID, userID string) error { - return nil -} - -// ensureFriendRelationship reserves centralized relation validation for -// transfer packets. -func (s *RedPacketService) ensureFriendRelationship(ctx context.Context, creatorUserID, receiverUserID string) error { - return nil -} - -func (s *RedPacketService) 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, fmt.Errorf("unsupported chain_type: %s", 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, fmt.Errorf("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 buildEVMBindMessage(req *WalletBindChallengeRequest, challengeID, nonce string, issuedAt, expiresAt time.Time) string { - domain := strings.TrimSpace(req.Domain) - if domain == "" { - domain = "redpacket" - } - uri := strings.TrimSpace(req.URI) - 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(req.WalletAddress)) - b.WriteString("\n\n") - fmt.Fprintf(&b, "Bind wallet %s to user %s.\n", strings.TrimSpace(req.WalletAddress), strings.TrimSpace(req.UserID)) - fmt.Fprintf(&b, "URI: %s\n", uri) - fmt.Fprintf(&b, "Version: 1\n") - fmt.Fprintf(&b, "Chain ID: %d\n", req.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(req *WalletBindChallengeRequest, 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(req.WalletAddress), - strings.TrimSpace(req.UserID), - challengeID, - nonce, - req.ChainID, - issuedAt.Format(time.RFC3339), - expiresAt.Format(time.RFC3339), - ) -} - -func verifyEVMBindSignature(message, walletAddress, signature string) error { - if strings.TrimSpace(message) == "" { - return fmt.Errorf("bind message is empty") - } - if !common.IsHexAddress(walletAddress) { - return fmt.Errorf("invalid evm wallet address") - } - - sig, err := hex.DecodeString(strings.TrimPrefix(signature, "0x")) - if err != nil { - return fmt.Errorf("decode signature failed: %w", err) - } - if len(sig) != 65 { - return fmt.Errorf("invalid signature length: %d", len(sig)) - } - if sig[64] >= 27 { - sig[64] -= 27 - } - if sig[64] > 1 { - return fmt.Errorf("invalid signature recovery id") - } - - hash := crypto.Keccak256Hash([]byte(personalSignMessage(message))) - pubKey, err := crypto.SigToPub(hash.Bytes(), sig) - if err != nil { - return fmt.Errorf("recover signer failed: %w", err) - } - - recovered := crypto.PubkeyToAddress(*pubKey) - if !strings.EqualFold(recovered.Hex(), walletAddress) { - return fmt.Errorf("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) -} - -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 "", fmt.Errorf("unsupported chain_type: %s", chainType) - } -} - -func validateCreateScope(scopeType, groupID, receiverUserID string, receiverUserIDs []string) error { - switch scopeType { - case "GROUP": - if strings.TrimSpace(groupID) == "" { - return fmt.Errorf("group_id is required when scope_type=GROUP") - } - case "DIRECT": - if strings.TrimSpace(receiverUserID) == "" && len(receiverUserIDs) == 0 { - return fmt.Errorf("receiver_user_id or receiver_user_ids is required when scope_type=DIRECT") - } - } - return nil -} - -func encodeReceiverUserIDs(userIDs []string) (string, error) { - if len(userIDs) == 0 { - return "", nil - } - encoded, err := json.Marshal(userIDs) - if err != nil { - return "", err - } - return string(encoded), nil -} - -func decodeReceiverUserIDs(raw string) []string { - if strings.TrimSpace(raw) == "" { - return nil - } - var userIDs []string - if err := json.Unmarshal([]byte(raw), &userIDs); err != nil { - return nil - } - return userIDs -} - -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 "" -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket_test.go b/cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket_test.go deleted file mode 100644 index 56f4a85d0..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket_test.go +++ /dev/null @@ -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) - } -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/main.go b/cmd/openim-rpc/openim-rpc-redpacket/main.go index 8cc704efc..ab3cfb7df 100644 --- a/cmd/openim-rpc/openim-rpc-redpacket/main.go +++ b/cmd/openim-rpc/openim-rpc-redpacket/main.go @@ -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) } } diff --git a/cmd/openim-rpc/openim-rpc-redpacket/pkg/resp/resp.go b/cmd/openim-rpc/openim-rpc-redpacket/pkg/resp/resp.go deleted file mode 100644 index d8f289ce7..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/pkg/resp/resp.go +++ /dev/null @@ -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) -} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/redpacket.db b/cmd/openim-rpc/openim-rpc-redpacket/redpacket.db deleted file mode 100644 index 69d24a4809538d989e05c8a2d0fc08e90e5a6457..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49152 zcmeI)&u-#I90zcF$;SK%p;c8`)m|)dXi#8dAh02aZqqKRqHNe~8kFW_Pw-^10w%G? zUA8x}?V+#GH>lJH>SOE!?4bjOWH2U4yS)T`El}(kduIIk{l=Dsl^DaTdTy66 ztt@>e$+GmVrb&{N;@=hioh(VdvNYM?e`R5P-fBvE`t$E;UYk<>kXHTGZ2GTsc_o&* zOr9kEOvK_kFUA4^2tWV=5P-n7z~GCNqV5;vLB)0~+PAH~;Zn=EAgy!C`0APOSl(pQ zwr1;AzCWz*9M!em#*6w-nt4|PruM3#nX}E9Yv#C5+cI;5gQTJsi}KYTBc~lYyZdZO zYCMjko$p@OBYV)+_~=Z{b{Ks_UG3m-cYo*bMBA&MXgkfLSGx_~!hXGRlov*#G1_N& zKHi~&!FuMnfc1?w@!FG3((U;Un{2gQN*G5Zyu{*b%VP86YOAz6gd_}qjMb}~O9X-3UZcajytuqRnX9IMwg zJW6?528m`-Vs&hXUZ-i^^z1hdVZO`Df_;2%xWD6fscX0LW^}e?mKAkxLmm|8#_^U! z^OU=DOgKC`Ufy(OT@@ZH>c)orYa%jIUN}hJ^6l~Q{)f;QZ%L;^ujlP-gn~>6P4*zJ ze0on!`qNIYb#CyeoDM@izR2+CF&;ZU)*y6IvC5nN#qcT-x?a3pQq<>JdGJ+400sLS zHt+FmK@$!Mhbt5)KgMSi@Aye%$EWr$hIkwBmd3r_HqSjHI4r8?aZT>v(XzU}E}x9D z!@KP8=S4meOy-5Y*zm5IFxZhu6d89+7fO||0cgC zXBPzd<~}xwdYjk|R}<2r)pM9jTFf98zo&ZB3DR>7KF8hB5klVZ;^}-Hcf{UZh}R3& zJEzVh;aHE6PAF4@GOtZ|$HI_k|H5|Pjc!`k*&E9I|D4RjXLC98jl@4#AOHafKmY;| zfB*y_009U<00Iz*6c{|0KbM;6cv>ZibXkt4pX~2fTD9`CGo2J@&7y_Pt#YMMtI%qJ z5?b4`&aA3VsbAFfqTVmwchP5-mwN$OF z>s#BpKDWLa)CbT1AEeBO$P+{%1Rwwb2tWV=5P$##AOHafKmY;{P+(P#WfQ`yDZ`ig z<;>p_|6qXt1Rwwb2tWV=5P$##AOHafK;VH1d>v~h?(-%>@cjQ#%6xoaCy3!f00Izz z00bZa0SG_<0uX=z1R!uPfmke^7{33{|LGqV2tWV=5P$##AOHafKmY;|fB*y*SYY`3 zKc4>=IKVhH2tWV=5P$##AOHafKmY;|fB+Z3@Bc^x2tWV=5P$##AOHafKmY;|fWYDl z;P?Nbz6 diff --git a/cmd/openim-rpc/openim-rpc-redpacket/router/router.go b/cmd/openim-rpc/openim-rpc-redpacket/router/router.go deleted file mode 100644 index cdceafca5..000000000 --- a/cmd/openim-rpc/openim-rpc-redpacket/router/router.go +++ /dev/null @@ -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) - } -} diff --git a/config/openim-rpc-redpacket.yml b/config/openim-rpc-redpacket.yml new file mode 100644 index 000000000..58bed2edf --- /dev/null +++ b/config/openim-rpc-redpacket.yml @@ -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 diff --git a/config/share.yml b/config/share.yml index 610bad52f..fa06e5607 100644 --- a/config/share.yml +++ b/config/share.yml @@ -12,6 +12,7 @@ rpcRegisterName: captcha: captcha rtc: rtc crypto: crypto + redPacket: redPacket imAdminUserID: [ imAdmin ] diff --git a/go.mod b/go.mod index 6f54e0c62..46e6fc9ef 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a84f11fdb..eb1d671e3 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/api/captcha.go b/internal/api/captcha.go index 9cedb3d3e..da311703b 100644 --- a/internal/api/captcha.go +++ b/internal/api/captcha.go @@ -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 } diff --git a/internal/api/init.go b/internal/api/init.go index f236450f9..e11ae21c7 100644 --- a/internal/api/init.go +++ b/internal/api/init.go @@ -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") diff --git a/internal/api/redpacket.go b/internal/api/redpacket.go new file mode 100644 index 000000000..62f50a9ac --- /dev/null +++ b/internal/api/redpacket.go @@ -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) +} diff --git a/internal/api/router.go b/internal/api/router.go index f3409ed99..9e94a8098 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -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) diff --git a/internal/rpc/captcha/captcha.go b/internal/rpc/captcha/captcha.go index 206b376e5..f438fd687 100644 --- a/internal/rpc/captcha/captcha.go +++ b/internal/rpc/captcha/captcha.go @@ -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 } diff --git a/internal/rpc/redpacket/admin.go b/internal/rpc/redpacket/admin.go new file mode 100644 index 000000000..5b459e28f --- /dev/null +++ b/internal/rpc/redpacket/admin.go @@ -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) +} diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/abi/RedPacket.json b/internal/rpc/redpacket/chain/abi/RedPacket.json similarity index 100% rename from cmd/openim-rpc/openim-rpc-redpacket/internal/chain/abi/RedPacket.json rename to internal/rpc/redpacket/chain/abi/RedPacket.json diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/client.go b/internal/rpc/redpacket/chain/client.go similarity index 88% rename from cmd/openim-rpc/openim-rpc-redpacket/internal/chain/client.go rename to internal/rpc/redpacket/chain/client.go index 5228fd2b2..0057545c3 100644 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/client.go +++ b/internal/rpc/redpacket/chain/client.go @@ -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") diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/indexer.go b/internal/rpc/redpacket/chain/indexer.go similarity index 62% rename from cmd/openim-rpc/openim-rpc-redpacket/internal/chain/indexer.go rename to internal/rpc/redpacket/chain/indexer.go index c41ff9906..0ea5525ff 100644 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/indexer.go +++ b/internal/rpc/redpacket/chain/indexer.go @@ -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") } diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/parser.go b/internal/rpc/redpacket/chain/parser.go similarity index 85% rename from cmd/openim-rpc/openim-rpc-redpacket/internal/chain/parser.go rename to internal/rpc/redpacket/chain/parser.go index fcf7a1f46..a3e53113a 100644 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/parser.go +++ b/internal/rpc/redpacket/chain/parser.go @@ -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 { diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/parser_test.go b/internal/rpc/redpacket/chain/parser_test.go similarity index 100% rename from cmd/openim-rpc/openim-rpc-redpacket/internal/chain/parser_test.go rename to internal/rpc/redpacket/chain/parser_test.go diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron.go b/internal/rpc/redpacket/chain/tron.go similarity index 91% rename from cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron.go rename to internal/rpc/redpacket/chain/tron.go index 11fd8167f..93f965522 100644 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron.go +++ b/internal/rpc/redpacket/chain/tron.go @@ -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 { diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron_indexer.go b/internal/rpc/redpacket/chain/tron_indexer.go similarity index 64% rename from cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron_indexer.go rename to internal/rpc/redpacket/chain/tron_indexer.go index 5b524a9e0..be7b1f2b8 100644 --- a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron_indexer.go +++ b/internal/rpc/redpacket/chain/tron_indexer.go @@ -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 } diff --git a/cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron_test.go b/internal/rpc/redpacket/chain/tron_test.go similarity index 100% rename from cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron_test.go rename to internal/rpc/redpacket/chain/tron_test.go diff --git a/internal/rpc/redpacket/redpacket.go b/internal/rpc/redpacket/redpacket.go new file mode 100644 index 000000000..1a9d7f653 --- /dev/null +++ b/internal/rpc/redpacket/redpacket.go @@ -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 +} diff --git a/internal/rpc/redpacket/service.go b/internal/rpc/redpacket/service.go new file mode 100644 index 000000000..22e2f15dc --- /dev/null +++ b/internal/rpc/redpacket/service.go @@ -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 +} diff --git a/internal/rpc/redpacket/wallet.go b/internal/rpc/redpacket/wallet.go new file mode 100644 index 000000000..569d96e84 --- /dev/null +++ b/internal/rpc/redpacket/wallet.go @@ -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) +} diff --git a/pkg/common/cmd/constant.go b/pkg/common/cmd/constant.go index dd770f688..d2f0ad852 100644 --- a/pkg/common/cmd/constant.go +++ b/pkg/common/cmd/constant.go @@ -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 { diff --git a/pkg/common/cmd/rpc_redpacket.go b/pkg/common/cmd/rpc_redpacket.go new file mode 100644 index 000000000..bdeef818f --- /dev/null +++ b/pkg/common/cmd/rpc_redpacket.go @@ -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) +} diff --git a/pkg/common/config/config.go b/pkg/common/config/config.go index 52ddb7cac..4857e5a99 100644 --- a/pkg/common/config/config.go +++ b/pkg/common/config/config.go @@ -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 } diff --git a/pkg/common/storage/controller/redpacket.go b/pkg/common/storage/controller/redpacket.go new file mode 100644 index 000000000..d052a9008 --- /dev/null +++ b/pkg/common/storage/controller/redpacket.go @@ -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) +} diff --git a/pkg/common/storage/database/mgo/redpacket.go b/pkg/common/storage/database/mgo/redpacket.go new file mode 100644 index 000000000..bf0579228 --- /dev/null +++ b/pkg/common/storage/database/mgo/redpacket.go @@ -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 +} diff --git a/pkg/common/storage/database/redpacket.go b/pkg/common/storage/database/redpacket.go new file mode 100644 index 000000000..dff792fcc --- /dev/null +++ b/pkg/common/storage/database/redpacket.go @@ -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) +} diff --git a/pkg/common/storage/model/redpacket.go b/pkg/common/storage/model/redpacket.go new file mode 100644 index 000000000..1b014b93c --- /dev/null +++ b/pkg/common/storage/model/redpacket.go @@ -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"` +} diff --git a/pkg/rpcli/redpacket.go b/pkg/rpcli/redpacket.go new file mode 100644 index 000000000..a3e73628f --- /dev/null +++ b/pkg/rpcli/redpacket.go @@ -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 +} diff --git a/protocol b/protocol index 90aae1d57..9f69daaff 160000 --- a/protocol +++ b/protocol @@ -1 +1 @@ -Subproject commit 90aae1d576466a1fa55eba386d1f7a38ca6062d0 +Subproject commit 9f69daaff1f7b46b971bb7b97cd993cd6302b41e