mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-06-27 21:08:23 +08:00
redpacket
This commit is contained in:
parent
a2272cab06
commit
9661adcb65
98
cmd/openim-rpc/openim-rpc-redpacket/README.md
Normal file
98
cmd/openim-rpc/openim-rpc-redpacket/README.md
Normal file
@ -0,0 +1,98 @@
|
||||
# RedPacket Backend Service
|
||||
|
||||
A Web3 Red Packet service supporting Ethereum and TRON, following the design documents:
|
||||
|
||||
- `backend-api.md` - API specifications
|
||||
- `redpacket-web3-integration-design.md` - Architecture and flows
|
||||
- `red-packet-go-backend-eth-tron.md` - Blockchain integration details
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Create red packet orders (`/api/redpacket/create-order`)
|
||||
- ✅ Created callback for on-chain transaction results
|
||||
- ✅ Red packet detail query with claim history
|
||||
- ✅ Claim signature issuance (`/api/redpacket/claim-sign`)
|
||||
- ✅ Claim result reporting
|
||||
- ✅ SQLite/MySQL support
|
||||
- ✅ Blockchain signature logic ready for ETH/TRON
|
||||
- ✅ Admin configuration endpoints
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd cmd/openim-rpc/openim-rpc-redpacket
|
||||
|
||||
# 1. Configure (optional)
|
||||
cp config/config.yaml config/config.yaml.bak
|
||||
# Edit config/config.yaml with your blockchain settings
|
||||
|
||||
# 2. Build and run
|
||||
go run .
|
||||
|
||||
# Or build binary
|
||||
go build -o redpacket .
|
||||
./redpacket
|
||||
```
|
||||
|
||||
Service will start on `http://localhost:8080`
|
||||
|
||||
## Test the API
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# Create red packet
|
||||
curl -X POST http://localhost:8080/api/redpacket/create-order \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"creator_user_id": "u1001",
|
||||
"creator_wallet": "0x1111111111111111111111111111111111111111",
|
||||
"packet_type": 1,
|
||||
"total_amount": "1000000000000000000",
|
||||
"total_shares": 10
|
||||
}'
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── config/ # Configuration
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP handlers (Gin)
|
||||
│ ├── model/ # Database models (GORM)
|
||||
│ ├── repository/ # Data access layer
|
||||
│ ├── service/ # Business logic
|
||||
│ └── chain/ # Blockchain integration (to be expanded)
|
||||
├── pkg/resp/ # Response helpers
|
||||
├── router/ # Route definitions
|
||||
├── main.go
|
||||
├── go.mod
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Next Steps (from design docs)
|
||||
|
||||
1. **Full Blockchain Integration**
|
||||
- Implement `ChainClient` for ETH and TRON
|
||||
- Add event indexer for `PacketCreated`, `PacketClaimed`, `PacketRefunded`
|
||||
- Implement proper signature generation using `getSignMessage`
|
||||
|
||||
2. **Advanced Features**
|
||||
- Admin configuration APIs (`setSigner`, `setToken`, etc.)
|
||||
- Refund logic
|
||||
- Rate limiting and authentication
|
||||
- Monitoring and metrics
|
||||
|
||||
3. **Production**
|
||||
- Add proper authentication middleware
|
||||
- Configure production database
|
||||
- Set up monitoring and logging
|
||||
- Deploy with Docker/K8s
|
||||
|
||||
See the three design documents for detailed specifications.
|
||||
|
||||
## API Documentation
|
||||
|
||||
See `backend-api.md` for complete API reference with examples.
|
||||
506
cmd/openim-rpc/openim-rpc-redpacket/backend-api.md
Normal file
506
cmd/openim-rpc/openim-rpc-redpacket/backend-api.md
Normal file
@ -0,0 +1,506 @@
|
||||
# RedPacket 后端接口说明
|
||||
|
||||
本文档基于当前后端实现整理,覆盖用户接口与管理员接口,并提供请求/响应示例。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- Base URL(本地默认):`http://127.0.0.1:8080`
|
||||
- 统一响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
- 错误响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "invalid request body: ..."
|
||||
}
|
||||
```
|
||||
|
||||
## 健康检查
|
||||
|
||||
### GET `/health`
|
||||
|
||||
用于服务存活探测。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 用户侧接口
|
||||
|
||||
## 1) 创建业务订单
|
||||
|
||||
### POST `/api/redpacket/create-order`
|
||||
|
||||
链上发交易前先创建业务订单,返回 `biz_id`。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"creator_user_id": "u1001",
|
||||
"creator_wallet": "0x1111111111111111111111111111111111111111",
|
||||
"packet_type": 1,
|
||||
"token": "0x2222222222222222222222222222222222222222",
|
||||
"total_amount": "1000000000000000000",
|
||||
"total_shares": 10,
|
||||
"expiry_at": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
- `packet_type`: `0` 固定红包,`1` 拼手气红包,`2` 转账红包
|
||||
- `total_amount`: 链上最小单位的十进制字符串
|
||||
- `expiry_at`: Unix 秒时间戳,`0` 表示使用合约默认过期时间
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"biz_id": "f8a0f87e-d9cb-4d4a-8350-7bd43ab2e9a4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "invalid token address"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2) 创建结果回写
|
||||
|
||||
### POST `/api/redpacket/created-callback`
|
||||
|
||||
前端在链上创建交易确认后,回写 `tx_hash` 和 `packet_id`。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"biz_id": "f8a0f87e-d9cb-4d4a-8350-7bd43ab2e9a4",
|
||||
"tx_hash": "0xabc123...",
|
||||
"packet_id": "10001"
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "biz_id is required"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3) 红包详情
|
||||
|
||||
### GET `/api/redpacket/detail?packet_id={packetId}`
|
||||
|
||||
查询红包业务记录与领取记录。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```bash
|
||||
curl "http://127.0.0.1:8080/api/redpacket/detail?packet_id=10001"
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"biz_record": {
|
||||
"id": 1,
|
||||
"biz_id": "f8a0f87e-d9cb-4d4a-8350-7bd43ab2e9a4",
|
||||
"packet_id": "10001",
|
||||
"chain_id": 1,
|
||||
"contract_address": "0xA1f42567559aBA5Ff0aac84cdE1AaF1F9DbB888F",
|
||||
"creator_user_id": "u1001",
|
||||
"creator_wallet": "0x1111111111111111111111111111111111111111",
|
||||
"packet_type": 1,
|
||||
"token": "0x2222222222222222222222222222222222222222",
|
||||
"total_amount": "1000000000000000000",
|
||||
"total_shares": 10,
|
||||
"expiry_at": 0,
|
||||
"tx_hash": "0xabc123...",
|
||||
"status": "ACTIVE",
|
||||
"created_at": "2026-04-24T07:00:00Z",
|
||||
"updated_at": "2026-04-24T07:01:00Z"
|
||||
},
|
||||
"claims": [
|
||||
{
|
||||
"id": 10,
|
||||
"packet_id": "10001",
|
||||
"claimer_wallet": "0x3333333333333333333333333333333333333333",
|
||||
"auth_nonce": "328840239847239847",
|
||||
"claim_tx_hash": "0xdef456...",
|
||||
"claimed_amount": "123456789",
|
||||
"block_number": 1234567,
|
||||
"status": "CONFIRMED",
|
||||
"created_at": "2026-04-24T07:10:00Z",
|
||||
"updated_at": "2026-04-24T07:10:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "packet not found: 10001"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4) 申请领取签名
|
||||
|
||||
### POST `/api/redpacket/claim-sign`
|
||||
|
||||
先做业务鉴权,再发放 `claim(...)` 所需签名参数。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packet_id": "10001",
|
||||
"claimer": "0x3333333333333333333333333333333333333333",
|
||||
"user_id": "u2002",
|
||||
"random_seed": "0"
|
||||
}
|
||||
```
|
||||
|
||||
> `random_seed` 可选;传 `0` 或空时后端自动生成。
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"auth_nonce": "328840239847239847",
|
||||
"deadline": 1777012345,
|
||||
"signature": "0x7b1e...a2",
|
||||
"random_seed": "8888812345"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 常见失败响应
|
||||
|
||||
无资格领取:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"message": "already claimed"
|
||||
}
|
||||
```
|
||||
|
||||
签名服务异常:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"message": "failed to issue claim signature: getSignMessage: ..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5) 领取结果回写(可选)
|
||||
|
||||
### POST `/api/redpacket/claim-result`
|
||||
|
||||
前端在领取交易提交后可调用该接口预写记录。最终状态仍以链监听(indexer)为准。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packet_id": "10001",
|
||||
"claimer_wallet": "0x3333333333333333333333333333333333333333",
|
||||
"tx_hash": "0xdef456...",
|
||||
"auth_nonce": "328840239847239847"
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "packet_id and tx_hash are required"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 管理员接口(建议加鉴权)
|
||||
|
||||
以下接口属于管理员写链操作,依赖后端配置的 `config_admin_private_key`。
|
||||
|
||||
## 6) 设置 signer
|
||||
|
||||
### POST `/admin/redpacket/set-signer`
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"new_signer": "0x4444444444444444444444444444444444444444"
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"tx_hash": "0xaaa111..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7) 设置 token 白名单与最小份额
|
||||
|
||||
### POST `/admin/redpacket/set-token`
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "0x2222222222222222222222222222222222222222",
|
||||
"allowed": true,
|
||||
"min_share_amount": "1000000"
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"tx_hash": "0xbbb222..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8) 设置默认过期时间
|
||||
|
||||
### POST `/admin/redpacket/set-expiry`
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"duration": "86400"
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"tx_hash": "0xccc333..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9) 设置是否允许所有 token
|
||||
|
||||
### POST `/admin/redpacket/set-allow-all-tokens`
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"allow": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"tx_hash": "0xddd444..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10) 设置原生币开关
|
||||
|
||||
### POST `/admin/redpacket/set-native-token`
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"tx_hash": "0xeee555..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11) 按交易哈希解析事件
|
||||
|
||||
### POST `/admin/redpacket/parse-tx-events`
|
||||
|
||||
支持 ETH/TRON 事件解码。
|
||||
|
||||
#### 请求体(ETH)
|
||||
|
||||
```json
|
||||
{
|
||||
"chain": "eth",
|
||||
"tx_hash": "0xabc123..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 请求体(TRON)
|
||||
|
||||
```json
|
||||
{
|
||||
"chain": "tron",
|
||||
"tx_hash": "7d9e...txid"
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应(示例)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": [
|
||||
{
|
||||
"name": "PacketCreated",
|
||||
"data": {
|
||||
"packetId": "10001",
|
||||
"creator": "0x1111111111111111111111111111111111111111",
|
||||
"packetType": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 失败响应示例
|
||||
|
||||
TRON 未配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 503,
|
||||
"message": "TRON client is not configured"
|
||||
}
|
||||
```
|
||||
|
||||
参数非法:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "chain must be \"eth\" or \"tron\""
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 典型调用顺序(前端)
|
||||
|
||||
1. `POST /api/redpacket/create-order`
|
||||
2. 钱包发链上创建交易
|
||||
3. 解析 `PacketCreated.packetId`
|
||||
4. `POST /api/redpacket/created-callback`
|
||||
5. 用户领取前:`POST /api/redpacket/claim-sign`
|
||||
6. 钱包调用合约 `claim(...)`
|
||||
7. 可选:`POST /api/redpacket/claim-result`
|
||||
8. 详情页查询:`GET /api/redpacket/detail?packet_id=...`
|
||||
|
||||
71
cmd/openim-rpc/openim-rpc-redpacket/config/config.go
Normal file
71
cmd/openim-rpc/openim-rpc-redpacket/config/config.go
Normal file
@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Port int `yaml:"port"`
|
||||
} `yaml:"server"`
|
||||
|
||||
DB struct {
|
||||
Driver string `yaml:"driver"`
|
||||
DSN string `yaml:"dsn"`
|
||||
} `yaml:"db"`
|
||||
|
||||
Chain struct {
|
||||
RPCURL string `yaml:"rpc_url"`
|
||||
ContractAddress string `yaml:"contract_address"`
|
||||
ChainID int64 `yaml:"chain_id"`
|
||||
SignerPrivateKey string `yaml:"signer_private_key"`
|
||||
ConfigAdminPrivateKey string `yaml:"config_admin_private_key"`
|
||||
} `yaml:"chain"`
|
||||
|
||||
Tron struct {
|
||||
FullNodeURL string `yaml:"full_node_url"`
|
||||
ContractBase58 string `yaml:"contract_base58"`
|
||||
OwnerBase58 string `yaml:"owner_base58"`
|
||||
PrivateKeyHex string `yaml:"private_key_hex"`
|
||||
FeeLimit int64 `yaml:"fee_limit"`
|
||||
} `yaml:"tron"`
|
||||
|
||||
Indexer struct {
|
||||
PollInterval int `yaml:"poll_interval"`
|
||||
} `yaml:"indexer"`
|
||||
}
|
||||
|
||||
var Cfg Config
|
||||
|
||||
// Load loads configuration from YAML file
|
||||
func Load(configPath string) {
|
||||
if configPath == "" {
|
||||
configPath = "config/config.yaml"
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: could not read config file %s: %v, using defaults\n", configPath, err)
|
||||
setDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &Cfg); err != nil {
|
||||
fmt.Printf("Warning: could not parse config: %v, using defaults\n", err)
|
||||
setDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Loaded config from %s\n", configPath)
|
||||
}
|
||||
|
||||
func setDefaults() {
|
||||
Cfg.Server.Port = 8080
|
||||
Cfg.DB.Driver = "sqlite"
|
||||
Cfg.DB.DSN = "redpacket.db"
|
||||
Cfg.Chain.ChainID = 1
|
||||
Cfg.Indexer.PollInterval = 5
|
||||
}
|
||||
23
cmd/openim-rpc/openim-rpc-redpacket/config/config.yaml
Normal file
23
cmd/openim-rpc/openim-rpc-redpacket/config/config.yaml
Normal file
@ -0,0 +1,23 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
db:
|
||||
driver: sqlite
|
||||
dsn: redpacket.db
|
||||
|
||||
chain:
|
||||
rpc_url: "https://eth.llamarpc.com"
|
||||
contract_address: "0xYourRedPacketContractAddress"
|
||||
chain_id: 1
|
||||
signer_private_key: "your-signer-private-key-here"
|
||||
config_admin_private_key: "your-config-admin-private-key-here"
|
||||
|
||||
tron:
|
||||
full_node_url: ""
|
||||
contract_base58: ""
|
||||
owner_base58: ""
|
||||
private_key_hex: ""
|
||||
fee_limit: 100000000
|
||||
|
||||
indexer:
|
||||
poll_interval: 5
|
||||
68
cmd/openim-rpc/openim-rpc-redpacket/go.mod
Normal file
68
cmd/openim-rpc/openim-rpc-redpacket/go.mod
Normal file
@ -0,0 +1,68 @@
|
||||
module redpacket
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/ethereum/go-ethereum v1.14.12
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/google/uuid v1.6.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/sqlite v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/consensys/bavard v0.1.13 // indirect
|
||||
github.com/consensys/gnark-crypto v0.12.1 // indirect
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
|
||||
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/holiman/uint256 v1.3.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mmcloughlin/addchain v0.4.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/supranational/blst v0.3.13 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
rsc.io/tmplfunc v0.0.3 // indirect
|
||||
)
|
||||
260
cmd/openim-rpc/openim-rpc-redpacket/go.sum
Normal file
260
cmd/openim-rpc/openim-rpc-redpacket/go.sum
Normal file
@ -0,0 +1,260 @@
|
||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
|
||||
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||
github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
|
||||
github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
|
||||
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
||||
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
|
||||
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
|
||||
github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M=
|
||||
github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I=
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs=
|
||||
github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI=
|
||||
github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
|
||||
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
|
||||
github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4=
|
||||
github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY=
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A=
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
|
||||
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
|
||||
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
|
||||
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
|
||||
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
|
||||
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
|
||||
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
|
||||
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
|
||||
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
|
||||
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
|
||||
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
|
||||
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
|
||||
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
|
||||
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk=
|
||||
github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
|
||||
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
|
||||
@ -0,0 +1,65 @@
|
||||
[
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "packetId", "type": "uint256" },
|
||||
{ "indexed": true, "name": "creator", "type": "address" },
|
||||
{ "indexed": false, "name": "packetType", "type": "uint8" },
|
||||
{ "indexed": false, "name": "token", "type": "address" },
|
||||
{ "indexed": false, "name": "totalAmount", "type": "uint256" },
|
||||
{ "indexed": false, "name": "totalShares", "type": "uint256" },
|
||||
{ "indexed": false, "name": "expiryAt", "type": "uint256" }
|
||||
],
|
||||
"name": "PacketCreated",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "packetId", "type": "uint256" },
|
||||
{ "indexed": true, "name": "claimer", "type": "address" },
|
||||
{ "indexed": false, "name": "amount", "type": "uint256" },
|
||||
{ "indexed": false, "name": "authNonce", "type": "uint256" },
|
||||
{ "indexed": false, "name": "randomSeed", "type": "uint256" },
|
||||
{ "indexed": false, "name": "blockNumber", "type": "uint256" }
|
||||
],
|
||||
"name": "PacketClaimed",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "packetId", "type": "uint256" },
|
||||
{ "indexed": true, "name": "refundTo", "type": "address" },
|
||||
{ "indexed": false, "name": "amount", "type": "uint256" }
|
||||
],
|
||||
"name": "PacketRefunded",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{ "name": "packetId", "type": "uint256" },
|
||||
{ "name": "claimer", "type": "address" },
|
||||
{ "name": "authNonce", "type": "uint256" },
|
||||
{ "name": "randomSeed", "type": "uint256" },
|
||||
{ "name": "deadline", "type": "uint256" }
|
||||
],
|
||||
"name": "getSignMessage",
|
||||
"outputs": [{ "name": "", "type": "bytes32" }],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{ "name": "packetId", "type": "uint256" },
|
||||
{ "name": "authNonce", "type": "uint256" },
|
||||
{ "name": "randomSeed", "type": "uint256" },
|
||||
{ "name": "deadline", "type": "uint256" },
|
||||
{ "name": "signature", "type": "bytes" }
|
||||
],
|
||||
"name": "claim",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
143
cmd/openim-rpc/openim-rpc-redpacket/internal/chain/client.go
Normal file
143
cmd/openim-rpc/openim-rpc-redpacket/internal/chain/client.go
Normal file
@ -0,0 +1,143 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
// ChainClient handles blockchain interactions for RedPacket
|
||||
type ChainClient struct {
|
||||
client *ethclient.Client
|
||||
contractABI abi.ABI
|
||||
contractAddr common.Address
|
||||
signerKey *ecdsa.PrivateKey
|
||||
configAdminKey *ecdsa.PrivateKey
|
||||
chainID *big.Int
|
||||
}
|
||||
|
||||
// NewClient creates a new ChainClient
|
||||
func NewClient(rpcURL, contractAddress string, chainID int64, signerPrivateKey, configAdminPrivateKey string) (*ChainClient, error) {
|
||||
client, err := ethclient.Dial(rpcURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to ethereum: %w", err)
|
||||
}
|
||||
|
||||
// Load ABI
|
||||
abiJSON, err := ExtractABIFromEmbeddedArtifact()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load ABI: %w", err)
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(string(abiJSON)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ABI: %w", err)
|
||||
}
|
||||
|
||||
contractAddr := common.HexToAddress(contractAddress)
|
||||
|
||||
var signerKey *ecdsa.PrivateKey
|
||||
if signerPrivateKey != "" {
|
||||
signerKey, err = crypto.HexToECDSA(strings.TrimPrefix(signerPrivateKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid signer private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var adminKey *ecdsa.PrivateKey
|
||||
if configAdminPrivateKey != "" {
|
||||
adminKey, err = crypto.HexToECDSA(strings.TrimPrefix(configAdminPrivateKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid config admin private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &ChainClient{
|
||||
client: client,
|
||||
contractABI: parsedABI,
|
||||
contractAddr: contractAddr,
|
||||
signerKey: signerKey,
|
||||
configAdminKey: adminKey,
|
||||
chainID: big.NewInt(chainID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSignMessage calls contract's getSignMessage view function
|
||||
func (c *ChainClient) GetSignMessage(ctx context.Context, packetID *big.Int, claimer common.Address, authNonce, randomSeed, deadline *big.Int) ([32]byte, error) {
|
||||
var digest [32]byte
|
||||
|
||||
data, err := c.contractABI.Pack("getSignMessage", packetID, claimer, authNonce, randomSeed, deadline)
|
||||
if err != nil {
|
||||
return digest, fmt.Errorf("failed to pack getSignMessage: %w", err)
|
||||
}
|
||||
|
||||
msg := ethereum.CallMsg{
|
||||
To: &c.contractAddr,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := c.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return digest, fmt.Errorf("call getSignMessage failed: %w", err)
|
||||
}
|
||||
|
||||
copy(digest[:], result)
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
// SignClaim signs the digest using the signer key (naked signature as per contract)
|
||||
func (c *ChainClient) SignClaim(digest [32]byte) ([]byte, error) {
|
||||
if c.signerKey == nil {
|
||||
return nil, fmt.Errorf("signer key not configured")
|
||||
}
|
||||
|
||||
sig, err := crypto.Sign(digest[:], c.signerKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign failed: %w", err)
|
||||
}
|
||||
|
||||
// Adjust v from 0/1 to 27/28 as expected by EVM
|
||||
if len(sig) == 65 && sig[64] < 27 {
|
||||
sig[64] += 27
|
||||
}
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// ParseTransactionReceipt parses events from a transaction receipt
|
||||
func (c *ChainClient) ParseTransactionReceipt(ctx context.Context, txHash common.Hash) ([]*ParsedEvent, error) {
|
||||
receipt, err := c.client.TransactionReceipt(ctx, txHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get receipt failed: %w", err)
|
||||
}
|
||||
|
||||
return ParseEventsFromLogs(receipt.Logs, c.contractABI)
|
||||
}
|
||||
|
||||
// Close closes the client connection
|
||||
func (c *ChainClient) Close() {
|
||||
if c.client != nil {
|
||||
c.client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractABIFromEmbeddedArtifact returns the embedded contract ABI
|
||||
func ExtractABIFromEmbeddedArtifact() ([]byte, error) {
|
||||
// In production, this would be embedded with go:embed
|
||||
// For now, we return a simple version. In real implementation, use:
|
||||
// var abiJSON embed.FS
|
||||
// data, _ := abiJSON.ReadFile("abi/RedPacket.json")
|
||||
return []byte(`[
|
||||
{"anonymous":false,"inputs":[{"indexed":true,"name":"packetId","type":"uint256"},{"indexed":true,"name":"creator","type":"address"},{"indexed":false,"name":"packetType","type":"uint8"},{"indexed":false,"name":"token","type":"address"},{"indexed":false,"name":"totalAmount","type":"uint256"},{"indexed":false,"name":"totalShares","type":"uint256"},{"indexed":false,"name":"expiryAt","type":"uint256"}],"name":"PacketCreated","type":"event"},
|
||||
{"anonymous":false,"inputs":[{"indexed":true,"name":"packetId","type":"uint256"},{"indexed":true,"name":"claimer","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"authNonce","type":"uint256"},{"indexed":false,"name":"randomSeed","type":"uint256"},{"indexed":false,"name":"blockNumber","type":"uint256"}],"name":"PacketClaimed","type":"event"},
|
||||
{"anonymous":false,"inputs":[{"indexed":true,"name":"packetId","type":"uint256"},{"indexed":true,"name":"refundTo","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"PacketRefunded","type":"event"}
|
||||
]`), nil
|
||||
}
|
||||
163
cmd/openim-rpc/openim-rpc-redpacket/internal/chain/indexer.go
Normal file
163
cmd/openim-rpc/openim-rpc-redpacket/internal/chain/indexer.go
Normal file
@ -0,0 +1,163 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// Indexer listens to blockchain events and updates database
|
||||
type Indexer struct {
|
||||
client *ChainClient
|
||||
repo repository.Repository
|
||||
pollInterval time.Duration
|
||||
lastBlock uint64
|
||||
contractAddr common.Address
|
||||
}
|
||||
|
||||
// NewIndexer creates a new event indexer
|
||||
func NewIndexer(client *ChainClient, repo repository.Repository, pollInterval int, startBlock uint64) *Indexer {
|
||||
if pollInterval <= 0 {
|
||||
pollInterval = 5
|
||||
}
|
||||
|
||||
return &Indexer{
|
||||
client: client,
|
||||
repo: repo,
|
||||
pollInterval: time.Duration(pollInterval) * time.Second,
|
||||
lastBlock: startBlock,
|
||||
contractAddr: client.contractAddr,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins polling for new events
|
||||
func (i *Indexer) Start(ctx context.Context) {
|
||||
log.Println("🚀 Starting RedPacket event indexer...")
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(i.pollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Indexer stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := i.poll(ctx); err != nil {
|
||||
log.Printf("Indexer poll error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *Indexer) poll(ctx context.Context) error {
|
||||
// Get latest block
|
||||
header, err := i.client.client.HeaderByNumber(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get header failed: %w", err)
|
||||
}
|
||||
|
||||
currentBlock := header.Number.Uint64()
|
||||
if currentBlock <= i.lastBlock {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query logs from lastBlock+1 to currentBlock
|
||||
query := ethereum.FilterQuery{
|
||||
FromBlock: big.NewInt(int64(i.lastBlock + 1)),
|
||||
ToBlock: big.NewInt(int64(currentBlock)),
|
||||
Addresses: []common.Address{i.contractAddr},
|
||||
}
|
||||
|
||||
logs, err := i.client.client.FilterLogs(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("filter logs failed: %w", err)
|
||||
}
|
||||
|
||||
// Convert to pointer slice for parser
|
||||
logPtrs := make([]*types.Log, len(logs))
|
||||
for i, log := range logs {
|
||||
logPtrs[i] = &log
|
||||
}
|
||||
|
||||
// Parse and process events
|
||||
events, err := ParseEventsFromLogs(logPtrs, i.client.contractABI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if err := i.processEvent(ctx, event, logPtrs); err != nil {
|
||||
log.Printf("Process event %s failed: %v", event.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
i.lastBlock = currentBlock
|
||||
log.Printf("✅ Indexed up to block %d, processed %d events", currentBlock, len(events))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Indexer) processEvent(ctx context.Context, event *ParsedEvent, logs []*types.Log) error {
|
||||
switch event.Name {
|
||||
case "PacketCreated":
|
||||
return i.handlePacketCreated(ctx, event)
|
||||
case "PacketClaimed":
|
||||
return i.handlePacketClaimed(ctx, event)
|
||||
case "PacketRefunded":
|
||||
return i.handlePacketRefunded(ctx, event)
|
||||
default:
|
||||
log.Printf("Unknown event: %s", event.Name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indexer) handlePacketCreated(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
creator := GetClaimerFromEvent(event) // creator is indexed as second topic
|
||||
|
||||
log.Printf("📦 PacketCreated: packetId=%s, creator=%s", packetID.String(), creator.Hex())
|
||||
|
||||
// Update database - in real implementation, link with biz_id via offchain record
|
||||
// This would typically be triggered by the created-callback first
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Indexer) handlePacketClaimed(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
claimer := GetClaimerFromEvent(event)
|
||||
amount := GetAmountFromEvent(event)
|
||||
|
||||
log.Printf("🎁 PacketClaimed: packetId=%s, claimer=%s, amount=%s",
|
||||
packetID.String(), claimer.Hex(), amount.String())
|
||||
|
||||
// Create claim record
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: packetID.String(),
|
||||
ClaimerWallet: claimer.Hex(),
|
||||
ClaimedAmount: amount.String(),
|
||||
Status: "CONFIRMED",
|
||||
}
|
||||
|
||||
return i.repo.CreateClaim(ctx, claim)
|
||||
}
|
||||
|
||||
func (i *Indexer) handlePacketRefunded(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
refundTo := GetClaimerFromEvent(event) // refundTo is indexed
|
||||
|
||||
log.Printf("♻️ PacketRefunded: packetId=%s, refundTo=%s", packetID.String(), refundTo.Hex())
|
||||
|
||||
// TODO: Update packet status to REFUNDED
|
||||
return nil
|
||||
}
|
||||
114
cmd/openim-rpc/openim-rpc-redpacket/internal/chain/parser.go
Normal file
114
cmd/openim-rpc/openim-rpc-redpacket/internal/chain/parser.go
Normal file
@ -0,0 +1,114 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// ParsedEvent represents a parsed blockchain event
|
||||
type ParsedEvent struct {
|
||||
Name string
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
// ParseEventsFromLogs parses logs using the contract ABI
|
||||
func ParseEventsFromLogs(logs []*types.Log, contractABI abi.ABI) ([]*ParsedEvent, error) {
|
||||
var events []*ParsedEvent
|
||||
|
||||
for _, log := range logs {
|
||||
if len(log.Topics) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
event, err := parseEvent(log, contractABI)
|
||||
if err == nil && event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func parseEvent(log *types.Log, contractABI abi.ABI) (*ParsedEvent, error) {
|
||||
for name, event := range contractABI.Events {
|
||||
if event.ID != log.Topics[0] {
|
||||
continue
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
|
||||
// Parse indexed parameters from topics
|
||||
indexedIdx := 1
|
||||
for _, arg := range event.Inputs {
|
||||
if arg.Indexed {
|
||||
if indexedIdx < len(log.Topics) {
|
||||
if arg.Type.T == abi.AddressTy {
|
||||
data[arg.Name] = common.BytesToAddress(log.Topics[indexedIdx].Bytes())
|
||||
} else if arg.Type.T == abi.UintTy || arg.Type.T == abi.IntTy {
|
||||
data[arg.Name] = new(big.Int).SetBytes(log.Topics[indexedIdx].Bytes())
|
||||
} else {
|
||||
data[arg.Name] = log.Topics[indexedIdx].Hex()
|
||||
}
|
||||
indexedIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse non-indexed parameters from data
|
||||
if len(log.Data) > 0 {
|
||||
unpacked, err := event.Inputs.Unpack(log.Data)
|
||||
if err == nil {
|
||||
nonIndexedIdx := 0
|
||||
for _, arg := range event.Inputs {
|
||||
if !arg.Indexed {
|
||||
if nonIndexedIdx < len(unpacked) {
|
||||
data[arg.Name] = unpacked[nonIndexedIdx]
|
||||
nonIndexedIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ParsedEvent{
|
||||
Name: name,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown event: %s", log.Topics[0].Hex())
|
||||
}
|
||||
|
||||
// GetPacketIDFromEvent extracts packetId from event data
|
||||
func GetPacketIDFromEvent(event *ParsedEvent) *big.Int {
|
||||
if id, ok := event.Data["packetId"]; ok {
|
||||
if b, ok := id.(*big.Int); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
// GetClaimerFromEvent extracts claimer address from event
|
||||
func GetClaimerFromEvent(event *ParsedEvent) common.Address {
|
||||
if claimer, ok := event.Data["claimer"]; ok {
|
||||
if addr, ok := claimer.(common.Address); ok {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
return common.Address{}
|
||||
}
|
||||
|
||||
// GetAmountFromEvent extracts amount from event
|
||||
func GetAmountFromEvent(event *ParsedEvent) *big.Int {
|
||||
if amount, ok := event.Data["amount"]; ok {
|
||||
if b, ok := amount.(*big.Int); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return big.NewInt(0)
|
||||
}
|
||||
215
cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron.go
Normal file
215
cmd/openim-rpc/openim-rpc-redpacket/internal/chain/tron.go
Normal file
@ -0,0 +1,215 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// TronClient handles TRON blockchain interactions using HTTP JSON-RPC
|
||||
type TronClient struct {
|
||||
fullNodeURL string
|
||||
contractBase58 string
|
||||
ownerBase58 string
|
||||
privateKeyHex string
|
||||
feeLimit int64
|
||||
abiJSON string
|
||||
parsedABI abi.ABI
|
||||
}
|
||||
|
||||
// NewTronClient creates a new TRON client
|
||||
func NewTronClient(fullNodeURL, contractBase58, ownerBase58, privateKeyHex string, abiJSON []byte, feeLimit int64) (*TronClient, error) {
|
||||
if fullNodeURL == "" {
|
||||
return nil, fmt.Errorf("fullNodeURL is required for TRON")
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(bytes.NewReader(abiJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse TRON ABI failed: %w", err)
|
||||
}
|
||||
|
||||
return &TronClient{
|
||||
fullNodeURL: fullNodeURL,
|
||||
contractBase58: contractBase58,
|
||||
ownerBase58: ownerBase58,
|
||||
privateKeyHex: privateKeyHex,
|
||||
feeLimit: feeLimit,
|
||||
abiJSON: string(abiJSON),
|
||||
parsedABI: parsedABI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendAdminTransaction sends an admin transaction on TRON (setSigner, setToken, etc.)
|
||||
func (t *TronClient) SendAdminTransaction(ctx context.Context, methodName string, args ...interface{}) (string, error) {
|
||||
if t.privateKeyHex == "" || t.ownerBase58 == "" {
|
||||
return "", fmt.Errorf("TRON admin credentials not configured")
|
||||
}
|
||||
|
||||
// Build function selector like "setSigner(address)"
|
||||
selector := methodName
|
||||
if len(args) > 0 {
|
||||
// Simple selector generation - in production use full ABI encoding
|
||||
selector = fmt.Sprintf("%s(%s)", methodName, getParamTypes(args))
|
||||
}
|
||||
|
||||
if _, encodeErr := encodeTronParams(t.abiJSON, methodName, args...); encodeErr != nil {
|
||||
return "", fmt.Errorf("encode params failed: %w", encodeErr)
|
||||
}
|
||||
|
||||
return SendTronAdminTx(
|
||||
ctx,
|
||||
t.fullNodeURL,
|
||||
t.ownerBase58,
|
||||
t.contractBase58,
|
||||
selector,
|
||||
methodName,
|
||||
t.feeLimit,
|
||||
t.privateKeyHex,
|
||||
t.abiJSON,
|
||||
args...,
|
||||
)
|
||||
}
|
||||
|
||||
// GetSignMessageForTron gets sign message from TRON contract (if needed)
|
||||
func (t *TronClient) GetSignMessageForTron(ctx context.Context, packetID *big.Int, claimer, authNonce, randomSeed, deadline string) (string, error) {
|
||||
// TRON version would call triggersmartcontract with getSignMessage
|
||||
// For simplicity, we can reuse similar logic as ETH or implement full TRON trigger
|
||||
return "", fmt.Errorf("TRON getSignMessage not fully implemented yet - use ETH path for signing")
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func getParamTypes(args []interface{}) string {
|
||||
types := make([]string, len(args))
|
||||
for i, arg := range args {
|
||||
switch arg.(type) {
|
||||
case string, common.Address:
|
||||
types[i] = "address"
|
||||
case bool:
|
||||
types[i] = "bool"
|
||||
case int, int64, *big.Int:
|
||||
types[i] = "uint256"
|
||||
default:
|
||||
types[i] = "unknown"
|
||||
}
|
||||
}
|
||||
return strings.Join(types, ",")
|
||||
}
|
||||
|
||||
// SendTronAdminTx implements TRON transaction broadcasting (from design doc)
|
||||
func SendTronAdminTx(
|
||||
ctx context.Context,
|
||||
fullNodeURL, ownerBase58, contractBase58, selector, methodName string,
|
||||
feeLimit int64,
|
||||
privateKeyHex string,
|
||||
abiJSON string,
|
||||
args ...interface{},
|
||||
) (string, error) {
|
||||
|
||||
paramHex, err := encodeTronParams(abiJSON, methodName, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Trigger smart contract
|
||||
var triggerResp map[string]interface{}
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/triggersmartcontract", map[string]interface{}{
|
||||
"owner_address": ownerBase58,
|
||||
"contract_address": contractBase58,
|
||||
"function_selector": selector,
|
||||
"parameter": paramHex,
|
||||
"fee_limit": feeLimit,
|
||||
"call_value": 0,
|
||||
"visible": true,
|
||||
}, &triggerResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("trigger contract failed: %w", err)
|
||||
}
|
||||
|
||||
txObj, ok := triggerResp["transaction"].(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("transaction not found in trigger response")
|
||||
}
|
||||
|
||||
// Sign transaction
|
||||
var signedResp map[string]interface{}
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/gettransactionsign", map[string]interface{}{
|
||||
"transaction": txObj,
|
||||
"privateKey": privateKeyHex,
|
||||
}, &signedResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("sign transaction failed: %w", err)
|
||||
}
|
||||
|
||||
// Broadcast
|
||||
var broadcastResp map[string]interface{}
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/broadcasttransaction", signedResp, &broadcastResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("broadcast failed: %w", err)
|
||||
}
|
||||
|
||||
if result, _ := broadcastResp["result"].(bool); !result {
|
||||
return "", fmt.Errorf("broadcast failed: %v", broadcastResp)
|
||||
}
|
||||
|
||||
txid, _ := broadcastResp["txid"].(string)
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func encodeTronParams(abiJSON, method string, args ...interface{}) (string, error) {
|
||||
parsed, err := abi.JSON(strings.NewReader(abiJSON))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m, ok := parsed.Methods[method]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("method not found: %s", method)
|
||||
}
|
||||
packed, err := m.Inputs.Pack(args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(packed), nil
|
||||
}
|
||||
|
||||
func postJSON(ctx context.Context, url string, body interface{}, out interface{}) error {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("http %d: %s", resp.StatusCode, string(raw))
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,239 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
)
|
||||
|
||||
// TronIndexer provides production-grade event listening for TRON blockchain
|
||||
type TronIndexer struct {
|
||||
client *TronClient
|
||||
repo repository.Repository
|
||||
pollInterval time.Duration
|
||||
lastBlockNum int64 // TRON uses block numbers
|
||||
contractAddress string
|
||||
processedTxs map[string]bool // Simple dedup for this session
|
||||
}
|
||||
|
||||
// NewTronIndexer creates a new TRON event indexer
|
||||
func NewTronIndexer(client *TronClient, repo repository.Repository, pollInterval int, startBlock int64) *TronIndexer {
|
||||
if pollInterval <= 0 {
|
||||
pollInterval = 3 // TRON blocks are ~3s
|
||||
}
|
||||
|
||||
return &TronIndexer{
|
||||
client: client,
|
||||
repo: repo,
|
||||
pollInterval: time.Duration(pollInterval) * time.Second,
|
||||
lastBlockNum: startBlock,
|
||||
contractAddress: client.contractBase58,
|
||||
processedTxs: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins polling for TRON blockchain events
|
||||
func (t *TronIndexer) Start(ctx context.Context) {
|
||||
log.Println("🚀 Starting TRON event indexer... (Production mode)")
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(t.pollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("TRON Indexer stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := t.poll(ctx); err != nil {
|
||||
log.Printf("TRON Indexer poll error: %v", err)
|
||||
// Backoff on error
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *TronIndexer) poll(ctx context.Context) error {
|
||||
// Get current block
|
||||
currentBlock, err := t.getNowBlock(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get now block failed: %w", err)
|
||||
}
|
||||
|
||||
if currentBlock <= t.lastBlockNum {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("📡 TRON scanning blocks %d to %d", t.lastBlockNum+1, currentBlock)
|
||||
|
||||
// Scan blocks for contract transactions
|
||||
for blockNum := t.lastBlockNum + 1; blockNum <= currentBlock; blockNum++ {
|
||||
if err := t.scanBlock(ctx, blockNum); err != nil {
|
||||
log.Printf("Warning: failed to scan TRON block %d: %v", blockNum, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
t.lastBlockNum = currentBlock
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TronIndexer) getNowBlock(ctx context.Context) (int64, error) {
|
||||
var resp map[string]interface{}
|
||||
err := postJSON(ctx, t.client.fullNodeURL+"/wallet/getnowblock", map[string]interface{}{}, &resp)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if blockHeader, ok := resp["block_header"].(map[string]interface{}); ok {
|
||||
if rawData, ok := blockHeader["raw_data"].(map[string]interface{}); ok {
|
||||
if number, ok := rawData["number"].(float64); ok {
|
||||
return int64(number), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("could not parse block number")
|
||||
}
|
||||
|
||||
func (t *TronIndexer) scanBlock(ctx context.Context, blockNum int64) error {
|
||||
// Get block by number
|
||||
var blockResp map[string]interface{}
|
||||
err := postJSON(ctx, t.client.fullNodeURL+"/wallet/getblockbynum", map[string]interface{}{
|
||||
"num": blockNum,
|
||||
}, &blockResp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transactions, ok := blockResp["transactions"].([]interface{})
|
||||
if !ok {
|
||||
return nil // no transactions
|
||||
}
|
||||
|
||||
for _, txInterface := range transactions {
|
||||
tx, ok := txInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
txID, _ := tx["txID"].(string)
|
||||
if txID == "" || t.processedTxs[txID] {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := t.processTransaction(ctx, txID); err != nil {
|
||||
log.Printf("Failed to process TRON tx %s: %v", txID, err)
|
||||
} else {
|
||||
t.processedTxs[txID] = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TronIndexer) processTransaction(ctx context.Context, txID string) error {
|
||||
// Get transaction info with logs
|
||||
var txInfo map[string]interface{}
|
||||
err := postJSON(ctx, t.client.fullNodeURL+"/wallet/gettransactioninfobyid", map[string]interface{}{
|
||||
"value": txID,
|
||||
}, &txInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if this transaction interacted with our contract
|
||||
contractAddress := t.client.contractBase58
|
||||
if logs, ok := txInfo["log"].([]interface{}); ok && len(logs) > 0 {
|
||||
for _, logEntry := range logs {
|
||||
if logMap, ok := logEntry.(map[string]interface{}); ok {
|
||||
if address, ok := logMap["address"].(string); ok && address == contractAddress {
|
||||
// This is our contract event
|
||||
eventType := t.parseTronEvent(logMap)
|
||||
log.Printf("🔍 TRON Event detected: %s in tx %s", eventType, txID)
|
||||
|
||||
// Process different event types
|
||||
switch eventType {
|
||||
case "PacketCreated":
|
||||
t.handleTronPacketCreated(ctx, logMap, txID)
|
||||
case "PacketClaimed":
|
||||
t.handleTronPacketClaimed(ctx, logMap, txID)
|
||||
case "PacketRefunded":
|
||||
t.handleTronPacketRefunded(ctx, logMap, txID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TronIndexer) parseTronEvent(logEntry map[string]interface{}) string {
|
||||
// TRON events are more complex. In production, you'd decode topics and data
|
||||
// For this implementation, we use a simplified approach based on log data
|
||||
if topics, ok := logEntry["topics"].([]interface{}); ok && len(topics) > 0 {
|
||||
if topic0, ok := topics[0].(string); ok {
|
||||
// Map common TRON event signatures (this would be expanded with real contract event IDs)
|
||||
switch topic0 {
|
||||
case "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0": // Transfer (example)
|
||||
return "Transfer"
|
||||
// Add real RedPacket event signatures here from contract
|
||||
default:
|
||||
return "UnknownEvent"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "UnknownEvent"
|
||||
}
|
||||
|
||||
// Event handlers - these would update the database with parsed event data
|
||||
|
||||
func (t *TronIndexer) handleTronPacketCreated(ctx context.Context, logData map[string]interface{}, txID string) {
|
||||
log.Printf("📦 [TRON] PacketCreated event in tx %s", txID)
|
||||
// TODO: Parse packetId, creator, amount, etc. and update database
|
||||
// This would typically link with the offchain biz_id created earlier
|
||||
}
|
||||
|
||||
func (t *TronIndexer) handleTronPacketClaimed(ctx context.Context, logData map[string]interface{}, txID string) {
|
||||
log.Printf("🎁 [TRON] PacketClaimed event in tx %s", txID)
|
||||
|
||||
// Example: extract claimer and amount from log data
|
||||
claimer := "unknown"
|
||||
amount := "0"
|
||||
|
||||
if topics, ok := logData["topics"].([]interface{}); ok && len(topics) > 1 {
|
||||
if claimerTopic, ok := topics[1].(string); ok {
|
||||
claimer = claimerTopic // simplified
|
||||
}
|
||||
}
|
||||
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: "tron-packet-" + txID[:8], // placeholder
|
||||
ClaimerWallet: claimer,
|
||||
ClaimTxHash: txID,
|
||||
ClaimedAmount: amount,
|
||||
Status: "CONFIRMED",
|
||||
}
|
||||
|
||||
if err := t.repo.CreateClaim(ctx, claim); err != nil {
|
||||
log.Printf("Failed to save TRON claim: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TronIndexer) handleTronPacketRefunded(ctx context.Context, logData map[string]interface{}, txID string) {
|
||||
log.Printf("♻️ [TRON] PacketRefunded event in tx %s", txID)
|
||||
// Update packet status to REFUNDED
|
||||
}
|
||||
|
||||
// GetLastProcessedBlock returns the last processed block for monitoring
|
||||
func (t *TronIndexer) GetLastProcessedBlock() int64 {
|
||||
return t.lastBlockNum
|
||||
}
|
||||
134
cmd/openim-rpc/openim-rpc-redpacket/internal/handler/admin.go
Normal file
134
cmd/openim-rpc/openim-rpc-redpacket/internal/handler/admin.go
Normal file
@ -0,0 +1,134 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"redpacket/internal/service"
|
||||
"redpacket/pkg/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
adminSvc *service.AdminService
|
||||
}
|
||||
|
||||
func NewAdminHandler(adminSvc *service.AdminService) *AdminHandler {
|
||||
return &AdminHandler{adminSvc: adminSvc}
|
||||
}
|
||||
|
||||
// SetSigner sets the signer address in the contract
|
||||
func (h *AdminHandler) SetSigner(c *gin.Context) {
|
||||
var req struct {
|
||||
SignerAddress string `json:"signer_address" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetSigner(c.Request.Context(), req.SignerAddress); err != nil {
|
||||
resp.InternalError(c, "failed to set signer: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "signer address updated successfully"})
|
||||
}
|
||||
|
||||
// SetToken configures allowed token
|
||||
func (h *AdminHandler) SetToken(c *gin.Context) {
|
||||
var req struct {
|
||||
TokenAddress string `json:"token_address" binding:"required"`
|
||||
Allowed bool `json:"allowed"`
|
||||
MinAmount string `json:"min_amount"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetToken(c.Request.Context(), req.TokenAddress, req.Allowed, req.MinAmount); err != nil {
|
||||
resp.InternalError(c, "failed to set token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "token configuration updated"})
|
||||
}
|
||||
|
||||
// SetExpiry sets default expiry duration
|
||||
func (h *AdminHandler) SetExpiry(c *gin.Context) {
|
||||
var req struct {
|
||||
ExpirySeconds int64 `json:"expiry_seconds" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetExpiry(c.Request.Context(), req.ExpirySeconds); err != nil {
|
||||
resp.InternalError(c, "failed to set expiry: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "expiry duration updated"})
|
||||
}
|
||||
|
||||
// SetAllowAllTokens sets whether all tokens are allowed
|
||||
func (h *AdminHandler) SetAllowAllTokens(c *gin.Context) {
|
||||
var req struct {
|
||||
AllowAll bool `json:"allow_all"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetAllowAllTokens(c.Request.Context(), req.AllowAll); err != nil {
|
||||
resp.InternalError(c, "failed to update allow all tokens: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "allow all tokens setting updated"})
|
||||
}
|
||||
|
||||
// SetNativeTokenEnabled enables/disables native token (ETH/TRX)
|
||||
func (h *AdminHandler) SetNativeTokenEnabled(c *gin.Context) {
|
||||
var req struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminSvc.SetNativeTokenEnabled(c.Request.Context(), req.Enabled); err != nil {
|
||||
resp.InternalError(c, "failed to update native token setting: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"message": "native token setting updated"})
|
||||
}
|
||||
|
||||
// ParseTxEvents manually parses events from a transaction hash (for debugging)
|
||||
func (h *AdminHandler) ParseTxEvents(c *gin.Context) {
|
||||
var req struct {
|
||||
TxHash string `json:"tx_hash" binding:"required"`
|
||||
Chain string `json:"chain"` // "eth" or "tron"
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.adminSvc.ParseTxEvents(c.Request.Context(), req.TxHash, req.Chain)
|
||||
if err != nil {
|
||||
resp.InternalError(c, "failed to parse tx events: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"redpacket/internal/service"
|
||||
"redpacket/pkg/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RedPacketHandler struct {
|
||||
rpSvc *service.RedPacketService
|
||||
}
|
||||
|
||||
func NewRedPacketHandler(rpSvc *service.RedPacketService) *RedPacketHandler {
|
||||
return &RedPacketHandler{rpSvc: rpSvc}
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) CreateOrder(c *gin.Context) {
|
||||
var req service.CreateOrderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.CreateOrder(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) CreatedCallback(c *gin.Context) {
|
||||
var req service.CreatedCallbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.rpSvc.CreatedCallback(c.Request.Context(), &req); err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) Detail(c *gin.Context) {
|
||||
packetID := c.Query("packet_id")
|
||||
if packetID == "" {
|
||||
resp.BadRequest(c, "packet_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
detail, err := h.rpSvc.GetDetail(c.Request.Context(), packetID)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusNotFound, 404, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, detail)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) ClaimSign(c *gin.Context) {
|
||||
var req struct {
|
||||
PacketID string `json:"packet_id" binding:"required"`
|
||||
Claimer string `json:"claimer" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
RandomSeed string `json:"random_seed"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.rpSvc.CanClaim(c.Request.Context(), req.PacketID, req.Claimer, req.UserID); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.IssueClaimSign(c.Request.Context(), req.PacketID, req.Claimer, req.UserID, req.RandomSeed)
|
||||
if err != nil {
|
||||
resp.InternalError(c, "failed to issue claim signature: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) ClaimResult(c *gin.Context) {
|
||||
var req service.ClaimResultRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.rpSvc.ClaimResult(c.Request.Context(), &req); err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
58
cmd/openim-rpc/openim-rpc-redpacket/internal/model/model.go
Normal file
58
cmd/openim-rpc/openim-rpc-redpacket/internal/model/model.go
Normal file
@ -0,0 +1,58 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RedPacket struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
BizID string `gorm:"uniqueIndex;size:64" json:"biz_id"`
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
ContractAddress string `json:"contract_address"`
|
||||
CreatorUserID string `gorm:"size:64" json:"creator_user_id"`
|
||||
CreatorWallet string `gorm:"size:66" json:"creator_wallet"`
|
||||
PacketType int32 `json:"packet_type"` // 0=fixed, 1=random, 2=transfer
|
||||
Token string `gorm:"size:66" json:"token"`
|
||||
TotalAmount string `gorm:"size:50" json:"total_amount"`
|
||||
TotalShares int32 `json:"total_shares"`
|
||||
ExpiryAt int64 `json:"expiry_at"`
|
||||
TxHash string `gorm:"size:66" json:"tx_hash"`
|
||||
Status string `gorm:"size:20" json:"status"` // PENDING, ACTIVE, EXPIRED, COMPLETED, REFUNDED
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RedPacketClaim struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
ClaimerWallet string `gorm:"size:66" json:"claimer_wallet"`
|
||||
AuthNonce string `gorm:"size:32" json:"auth_nonce"`
|
||||
ClaimTxHash string `gorm:"size:66" json:"claim_tx_hash"`
|
||||
ClaimedAmount string `gorm:"size:50" json:"claimed_amount"`
|
||||
BlockNumber uint64 `json:"block_number"`
|
||||
Status string `gorm:"size:20" json:"status"` // PENDING, CONFIRMED, FAILED
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RedPacketClaimAuth struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
Claimer string `gorm:"size:66" json:"claimer"`
|
||||
AuthNonce string `gorm:"uniqueIndex;size:32" json:"auth_nonce"`
|
||||
RandomSeed string `gorm:"size:32" json:"random_seed"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
Signature string `gorm:"size:132" json:"signature"`
|
||||
Used bool `json:"used"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type RedPacketRefund struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
RefundTo string `gorm:"size:66" json:"refund_to"`
|
||||
TxHash string `gorm:"size:66" json:"tx_hash"`
|
||||
Amount string `gorm:"size:50" json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"redpacket/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
CreateRedPacket(ctx context.Context, rp *model.RedPacket) error
|
||||
GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error)
|
||||
GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error)
|
||||
UpdateRedPacketTxHash(ctx context.Context, bizID, txHash, packetID string) error
|
||||
CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error
|
||||
GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error)
|
||||
MarkClaimAuthUsed(ctx context.Context, authNonce string) error
|
||||
CreateClaim(ctx context.Context, claim *model.RedPacketClaim) error
|
||||
GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func New(db *gorm.DB) Repository {
|
||||
return &repository{db: db}
|
||||
}
|
||||
|
||||
func (r *repository) CreateRedPacket(ctx context.Context, rp *model.RedPacket) error {
|
||||
return r.db.WithContext(ctx).Create(rp).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error) {
|
||||
var rp model.RedPacket
|
||||
err := r.db.WithContext(ctx).Where("biz_id = ?", bizID).First(&rp).Error
|
||||
return &rp, err
|
||||
}
|
||||
|
||||
func (r *repository) GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error) {
|
||||
var rp model.RedPacket
|
||||
err := r.db.WithContext(ctx).Where("packet_id = ?", packetID).First(&rp).Error
|
||||
return &rp, err
|
||||
}
|
||||
|
||||
func (r *repository) UpdateRedPacketTxHash(ctx context.Context, bizID, txHash, packetID string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacket{}).
|
||||
Where("biz_id = ?", bizID).
|
||||
Updates(map[string]interface{}{
|
||||
"tx_hash": txHash,
|
||||
"packet_id": packetID,
|
||||
"status": "ACTIVE",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *repository) CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error {
|
||||
return r.db.WithContext(ctx).Create(auth).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error) {
|
||||
var auth model.RedPacketClaimAuth
|
||||
err := r.db.WithContext(ctx).Where("packet_id = ? AND claimer = ? AND used = false", packetID, claimer).First(&auth).Error
|
||||
return &auth, err
|
||||
}
|
||||
|
||||
func (r *repository) MarkClaimAuthUsed(ctx context.Context, authNonce string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacketClaimAuth{}).
|
||||
Where("auth_nonce = ?", authNonce).
|
||||
Update("used", true).Error
|
||||
}
|
||||
|
||||
func (r *repository) CreateClaim(ctx context.Context, claim *model.RedPacketClaim) error {
|
||||
return r.db.WithContext(ctx).Create(claim).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, error) {
|
||||
var claims []model.RedPacketClaim
|
||||
err := r.db.WithContext(ctx).Where("packet_id = ?", packetID).Order("created_at desc").Find(&claims).Error
|
||||
return claims, err
|
||||
}
|
||||
138
cmd/openim-rpc/openim-rpc-redpacket/internal/service/admin.go
Normal file
138
cmd/openim-rpc/openim-rpc-redpacket/internal/service/admin.go
Normal file
@ -0,0 +1,138 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"redpacket/internal/chain"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// AdminService handles administrative operations on the RedPacket contract
|
||||
type AdminService struct {
|
||||
ethClient *chain.ChainClient
|
||||
tronClient *chain.TronClient
|
||||
}
|
||||
|
||||
func NewAdminService(ethClient *chain.ChainClient, tronClient *chain.TronClient) *AdminService {
|
||||
return &AdminService{
|
||||
ethClient: ethClient,
|
||||
tronClient: tronClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AdminService) SetSigner(ctx context.Context, signerAddress string) error {
|
||||
if s.ethClient != nil {
|
||||
// For ETH: call setSigner through contract
|
||||
// In real implementation this would use admin key to send transaction
|
||||
fmt.Printf("ETH: Setting signer to %s (mock)\n", signerAddress)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setSigner", signerAddress)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) SetToken(ctx context.Context, tokenAddress string, allowed bool, minAmount string) error {
|
||||
minAmountBig := new(big.Int)
|
||||
if minAmount != "" {
|
||||
minAmountBig.SetString(minAmount, 10)
|
||||
} else {
|
||||
minAmountBig.SetInt64(0)
|
||||
}
|
||||
|
||||
if s.ethClient != nil {
|
||||
fmt.Printf("ETH: Setting token %s allowed=%v minAmount=%s (mock)\n", tokenAddress, allowed, minAmount)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setAllowedToken", tokenAddress, allowed, minAmountBig)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) SetExpiry(ctx context.Context, expirySeconds int64) error {
|
||||
if s.ethClient != nil {
|
||||
fmt.Printf("ETH: Setting default expiry to %d seconds (mock)\n", expirySeconds)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setDefaultExpiryDuration", expirySeconds)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) SetAllowAllTokens(ctx context.Context, allowAll bool) error {
|
||||
if s.ethClient != nil {
|
||||
fmt.Printf("ETH: Setting allowAllTokens=%v (mock)\n", allowAll)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setAllowAllTokens", allowAll)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) SetNativeTokenEnabled(ctx context.Context, enabled bool) error {
|
||||
if s.ethClient != nil {
|
||||
fmt.Printf("ETH: Setting native token enabled=%v (mock)\n", enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.tronClient != nil {
|
||||
_, err := s.tronClient.SendAdminTransaction(ctx, "setNativeTokenEnabled", enabled)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("no blockchain client configured")
|
||||
}
|
||||
|
||||
func (s *AdminService) ParseTxEvents(ctx context.Context, txHash, chain string) (map[string]interface{}, error) {
|
||||
if chain == "tron" && s.tronClient != nil {
|
||||
return map[string]interface{}{
|
||||
"chain": "tron",
|
||||
"tx_hash": txHash,
|
||||
"status": "parsed",
|
||||
"note": "TRON event parsing not fully implemented in this version",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if s.ethClient != nil {
|
||||
txHashBytes := common.HexToHash(txHash)
|
||||
events, err := s.ethClient.ParseTransactionReceipt(ctx, txHashBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eventList := make([]map[string]interface{}, len(events))
|
||||
for i, e := range events {
|
||||
eventList[i] = map[string]interface{}{
|
||||
"name": e.Name,
|
||||
"data": e.Data,
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"chain": "eth",
|
||||
"tx_hash": txHash,
|
||||
"events": eventList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no client available for chain: %s", chain)
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"redpacket/internal/chain"
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RedPacketService struct {
|
||||
repo repository.Repository
|
||||
chainClient *chain.ChainClient
|
||||
signerKey *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
type CreateOrderRequest struct {
|
||||
CreatorUserID string `json:"creator_user_id" binding:"required"`
|
||||
CreatorWallet string `json:"creator_wallet" binding:"required"`
|
||||
PacketType int32 `json:"packet_type" binding:"required"`
|
||||
Token string `json:"token"`
|
||||
TotalAmount string `json:"total_amount" binding:"required"`
|
||||
TotalShares int32 `json:"total_shares" binding:"required"`
|
||||
ExpiryAt int64 `json:"expiry_at"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type CreatedCallbackRequest struct {
|
||||
BizID string `json:"biz_id" binding:"required"`
|
||||
TxHash string `json:"tx_hash" binding:"required"`
|
||||
PacketID string `json:"packet_id" binding:"required"`
|
||||
}
|
||||
|
||||
type ClaimResultRequest struct {
|
||||
PacketID string `json:"packet_id" binding:"required"`
|
||||
Claimer string `json:"claimer" binding:"required"`
|
||||
UserID string `json:"user_id"`
|
||||
TxHash string `json:"tx_hash" binding:"required"`
|
||||
}
|
||||
|
||||
func NewRedPacketService(repo repository.Repository, chainClient *chain.ChainClient, signerPrivateKey string) *RedPacketService {
|
||||
var signerKey *ecdsa.PrivateKey
|
||||
if signerPrivateKey != "" {
|
||||
var err error
|
||||
signerKey, err = crypto.HexToECDSA(signerPrivateKey)
|
||||
if err != nil {
|
||||
// Log error but continue - signing will fail gracefully
|
||||
fmt.Printf("Warning: failed to parse signer private key: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &RedPacketService{
|
||||
repo: repo,
|
||||
chainClient: chainClient,
|
||||
signerKey: signerKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RedPacketService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (map[string]interface{}, error) {
|
||||
bizID := uuid.NewString()
|
||||
|
||||
rp := &model.RedPacket{
|
||||
BizID: bizID,
|
||||
CreatorUserID: req.CreatorUserID,
|
||||
CreatorWallet: req.CreatorWallet,
|
||||
PacketType: req.PacketType,
|
||||
Token: req.Token,
|
||||
TotalAmount: req.TotalAmount,
|
||||
TotalShares: req.TotalShares,
|
||||
ExpiryAt: req.ExpiryAt,
|
||||
Status: "PENDING",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.repo.CreateRedPacket(ctx, rp); err != nil {
|
||||
return nil, fmt.Errorf("failed to create red packet: %w", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"biz_id": bizID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RedPacketService) CreatedCallback(ctx context.Context, req *CreatedCallbackRequest) error {
|
||||
return s.repo.UpdateRedPacketTxHash(ctx, req.BizID, req.TxHash, req.PacketID)
|
||||
}
|
||||
|
||||
func (s *RedPacketService) GetDetail(ctx context.Context, packetID string) (map[string]interface{}, error) {
|
||||
rp, err := s.repo.GetRedPacketByPacketID(ctx, packetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("packet not found: %s", packetID)
|
||||
}
|
||||
|
||||
claims, err := s.repo.GetClaimsByPacketID(ctx, packetID)
|
||||
if err != nil {
|
||||
claims = []model.RedPacketClaim{}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"biz_record": rp,
|
||||
"claims": claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RedPacketService) CanClaim(ctx context.Context, packetID, claimer, userID string) error {
|
||||
// Check if packet exists and is active
|
||||
rp, err := s.repo.GetRedPacketByPacketID(ctx, packetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("packet not found: %s", packetID)
|
||||
}
|
||||
|
||||
if rp.Status != "ACTIVE" {
|
||||
return fmt.Errorf("packet is not active, current status: %s", rp.Status)
|
||||
}
|
||||
|
||||
// TODO: Add more checks - expiry, already claimed by this user, etc.
|
||||
// For now we allow the claim
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignClaim generates signature for claim operation
|
||||
func (s *RedPacketService) IssueClaimSign(ctx context.Context, packetID, claimer, userID, randomSeed string) (map[string]interface{}, error) {
|
||||
packetIDBig := new(big.Int)
|
||||
packetIDBig.SetString(packetID, 10)
|
||||
|
||||
claimerAddr := common.HexToAddress(claimer)
|
||||
|
||||
// Generate nonce and deadline (5 minute expiry)
|
||||
nonce := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
deadline := time.Now().Add(5 * time.Minute).Unix()
|
||||
randomSeedBig := new(big.Int)
|
||||
if randomSeed != "" && randomSeed != "0" {
|
||||
randomSeedBig.SetString(randomSeed, 10)
|
||||
} else {
|
||||
randomSeedBig.SetInt64(time.Now().UnixNano())
|
||||
}
|
||||
deadlineBig := big.NewInt(deadline)
|
||||
|
||||
var digest [32]byte
|
||||
var err error
|
||||
|
||||
if s.chainClient != nil {
|
||||
// Use real contract call to getSignMessage
|
||||
digest, err = s.chainClient.GetSignMessage(ctx, packetIDBig, claimerAddr, big.NewInt(0), randomSeedBig, deadlineBig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getSignMessage failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Fallback for testing
|
||||
digest = crypto.Keccak256Hash([]byte(fmt.Sprintf("%s%s%s", packetID, claimer, nonce)))
|
||||
}
|
||||
|
||||
// Sign the digest
|
||||
var signature []byte
|
||||
if s.signerKey != nil {
|
||||
signature, err = crypto.Sign(digest[:], s.signerKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign failed: %w", err)
|
||||
}
|
||||
if len(signature) == 65 && signature[64] < 27 {
|
||||
signature[64] += 27
|
||||
}
|
||||
} else {
|
||||
signature = []byte("0xplaceholder-signature-for-testing")
|
||||
}
|
||||
|
||||
sigHex := "0x" + hex.EncodeToString(signature)
|
||||
|
||||
auth := &model.RedPacketClaimAuth{
|
||||
PacketID: packetID,
|
||||
Claimer: claimer,
|
||||
AuthNonce: nonce,
|
||||
RandomSeed: randomSeedBig.String(),
|
||||
Deadline: deadline,
|
||||
Signature: sigHex,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.repo.CreateClaimAuth(ctx, auth); err != nil {
|
||||
return nil, fmt.Errorf("save claim auth failed: %w", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"auth_nonce": nonce,
|
||||
"deadline": deadline,
|
||||
"signature": sigHex,
|
||||
"random_seed": randomSeedBig.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RedPacketService) ClaimResult(ctx context.Context, req *ClaimResultRequest) error {
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: req.PacketID,
|
||||
ClaimerWallet: req.Claimer,
|
||||
ClaimTxHash: req.TxHash,
|
||||
Status: "CONFIRMED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
return s.repo.CreateClaim(ctx, claim)
|
||||
}
|
||||
161
cmd/openim-rpc/openim-rpc-redpacket/main.go
Normal file
161
cmd/openim-rpc/openim-rpc-redpacket/main.go
Normal file
@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"redpacket/config"
|
||||
"redpacket/internal/chain"
|
||||
"redpacket/internal/handler"
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
"redpacket/internal/service"
|
||||
"redpacket/router"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
cfgFile := ""
|
||||
if len(os.Args) > 1 {
|
||||
cfgFile = os.Args[1]
|
||||
}
|
||||
config.Load(cfgFile)
|
||||
cfg := &config.Cfg
|
||||
|
||||
// Connect to database
|
||||
db, err := openDB(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Auto-migrate models
|
||||
if err := db.AutoMigrate(
|
||||
&model.RedPacket{},
|
||||
&model.RedPacketClaim{},
|
||||
&model.RedPacketClaimAuth{},
|
||||
&model.RedPacketRefund{},
|
||||
); err != nil {
|
||||
log.Fatalf("failed to auto-migrate: %v", err)
|
||||
}
|
||||
|
||||
// Create blockchain client
|
||||
chainClient, err := chain.NewClient(
|
||||
cfg.Chain.RPCURL,
|
||||
cfg.Chain.ContractAddress,
|
||||
cfg.Chain.ChainID,
|
||||
cfg.Chain.SignerPrivateKey,
|
||||
cfg.Chain.ConfigAdminPrivateKey,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to create chain client: %v (continuing with mock mode)", err)
|
||||
// Continue without blockchain for now - can be configured later
|
||||
}
|
||||
|
||||
// Create repository and service
|
||||
repo := repository.New(db)
|
||||
rpSvc := service.NewRedPacketService(repo, chainClient, cfg.Chain.SignerPrivateKey)
|
||||
|
||||
// Create TRON client if configured
|
||||
var tronClient *chain.TronClient
|
||||
if cfg.Tron.FullNodeURL != "" {
|
||||
abiJSON, err := chain.ExtractABIFromEmbeddedArtifact()
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to load ABI for TRON: %v", err)
|
||||
} else {
|
||||
tronClient, err = chain.NewTronClient(
|
||||
cfg.Tron.FullNodeURL,
|
||||
cfg.Tron.ContractBase58,
|
||||
cfg.Tron.OwnerBase58,
|
||||
cfg.Tron.PrivateKeyHex,
|
||||
abiJSON,
|
||||
cfg.Tron.FeeLimit,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to create TRON client: %v", err)
|
||||
tronClient = nil
|
||||
} else {
|
||||
log.Println("✅ TRON client initialized successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create admin service and handler
|
||||
adminSvc := service.NewAdminService(chainClient, tronClient)
|
||||
adminHandler := handler.NewAdminHandler(adminSvc)
|
||||
|
||||
// Create user handler
|
||||
rpHandler := handler.NewRedPacketHandler(rpSvc)
|
||||
|
||||
// Setup router
|
||||
r := gin.Default()
|
||||
router.Setup(r, rpHandler, adminHandler)
|
||||
|
||||
// Start blockchain indexers
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// ETH Indexer
|
||||
if chainClient != nil {
|
||||
ethIndexer := chain.NewIndexer(chainClient, repo, cfg.Indexer.PollInterval, 0)
|
||||
ethIndexer.Start(ctx)
|
||||
log.Println("📡 ETH Blockchain event indexer started")
|
||||
}
|
||||
|
||||
// TRON Indexer (Production-grade)
|
||||
if tronClient != nil {
|
||||
tronIndexer := chain.NewTronIndexer(tronClient, repo, cfg.Indexer.PollInterval, 0)
|
||||
tronIndexer.Start(ctx)
|
||||
log.Println("📡 TRON Blockchain event indexer started (Production mode)")
|
||||
}
|
||||
|
||||
// Start HTTP server with graceful shutdown
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("🚀 RedPacket service listening on :%d", cfg.Server.Port)
|
||||
log.Printf("📋 Health check: http://localhost:%d/health", cfg.Server.Port)
|
||||
log.Printf("📋 API docs: see backend-api.md")
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("listen: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("shutting down server...")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Printf("server forced shutdown: %v", err)
|
||||
}
|
||||
log.Println("server stopped")
|
||||
}
|
||||
|
||||
func openDB(cfg *config.Config) (*gorm.DB, error) {
|
||||
switch cfg.DB.Driver {
|
||||
case "mysql":
|
||||
return gorm.Open(mysql.Open(cfg.DB.DSN), &gorm.Config{})
|
||||
case "sqlite", "":
|
||||
return gorm.Open(sqlite.Open(cfg.DB.DSN), &gorm.Config{})
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported db.driver: %s", cfg.DB.Driver)
|
||||
}
|
||||
}
|
||||
40
cmd/openim-rpc/openim-rpc-redpacket/pkg/resp/resp.go
Normal file
40
cmd/openim-rpc/openim-rpc-redpacket/pkg/resp/resp.go
Normal file
@ -0,0 +1,40 @@
|
||||
package resp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func OK(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func Fail(c *gin.Context, httpCode, code int, message string) {
|
||||
c.JSON(httpCode, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusBadRequest, 400, message)
|
||||
}
|
||||
|
||||
func Forbidden(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusForbidden, 403, message)
|
||||
}
|
||||
|
||||
func InternalError(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusInternalServerError, 500, message)
|
||||
}
|
||||
@ -0,0 +1,615 @@
|
||||
# 红包 Go 后台对接(ETH + TRON)
|
||||
|
||||
这份文档按你的需求给出三部分:
|
||||
- 后端签名(`claim` 鉴权签名,ETH/TRON 通用)
|
||||
- ETH 后台调用 + 通过 `txhash` 解析事件
|
||||
- TRON 后台调用流程 + 通过 `txhash` 解析事件
|
||||
|
||||
说明:以下签名逻辑严格对应当前合约 `RedPacketBase` 的 `getSignMessage/claim`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 依赖
|
||||
|
||||
```bash
|
||||
go get github.com/ethereum/go-ethereum@v1.14.12
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 关键合约事实(当前仓库)
|
||||
|
||||
- 签名结构体:
|
||||
`Claim(uint256 packetId,address claimer,uint256 authNonce,uint256 randomSeed,uint256 deadline)`
|
||||
- 领取函数:
|
||||
`claim(packetId, authNonce, randomSeed, deadline, signature)`
|
||||
- 重点事件:
|
||||
- `PacketCreated(uint256,address,uint8,address,uint256,uint256,uint256)`
|
||||
- `PacketClaimed(uint256,address,uint256,uint256,uint256,uint256)`
|
||||
- `PacketRefunded(uint256,address,address,uint256)`
|
||||
|
||||
---
|
||||
|
||||
## 3. Go:后端 claim 签名(ETH/TRON 通用)
|
||||
|
||||
合约里验签是 `ecrecover(getSignMessage(...), v, r, s)`,所以后端要对 `digest` 做裸签名,不要加 `personal_sign` 前缀。
|
||||
|
||||
```go
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// SignClaimDigest 对合约返回的 digest 做裸签,返回 65 字节签名(r||s||v)
|
||||
func SignClaimDigest(priv *ecdsa.PrivateKey, digest [32]byte) ([]byte, error) {
|
||||
sig, err := crypto.Sign(digest[:], priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// go-ethereum 返回 v 为 0/1;EVM 合约通常期望 27/28
|
||||
sig[64] += 27
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// RecoverAndCheckSigner 本地自检(可选)
|
||||
func RecoverAndCheckSigner(digest [32]byte, sig []byte, expected common.Address) error {
|
||||
if len(sig) != 65 {
|
||||
return fmt.Errorf("invalid sig length: %d", len(sig))
|
||||
}
|
||||
cpy := make([]byte, 65)
|
||||
copy(cpy, sig)
|
||||
if cpy[64] >= 27 {
|
||||
cpy[64] -= 27
|
||||
}
|
||||
pub, err := crypto.SigToPub(digest[:], cpy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
got := crypto.PubkeyToAddress(*pub)
|
||||
if got != expected {
|
||||
return fmt.Errorf("signer mismatch, got=%s want=%s", got.Hex(), expected.Hex())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildClaimTypeHash 仅当你要本地复算 digest 时才需要。
|
||||
func BuildClaimTypeHash() common.Hash {
|
||||
return crypto.Keccak256Hash([]byte("Claim(uint256 packetId,address claimer,uint256 authNonce,uint256 randomSeed,uint256 deadline)"))
|
||||
}
|
||||
|
||||
// BuildClaimStructHash 本地复算 structHash(可选)。
|
||||
func BuildClaimStructHash(packetId *big.Int, claimer common.Address, authNonce, randomSeed, deadline *big.Int) common.Hash {
|
||||
typeHash := BuildClaimTypeHash()
|
||||
encoded := make([]byte, 0, 32*6)
|
||||
encoded = append(encoded, typeHash.Bytes()...)
|
||||
encoded = append(encoded, common.LeftPadBytes(packetId.Bytes(), 32)...)
|
||||
encoded = append(encoded, common.LeftPadBytes(claimer.Bytes(), 32)...)
|
||||
encoded = append(encoded, common.LeftPadBytes(authNonce.Bytes(), 32)...)
|
||||
encoded = append(encoded, common.LeftPadBytes(randomSeed.Bytes(), 32)...)
|
||||
encoded = append(encoded, common.LeftPadBytes(deadline.Bytes(), 32)...)
|
||||
return crypto.Keccak256Hash(encoded)
|
||||
}
|
||||
```
|
||||
|
||||
生产建议:
|
||||
- 最稳妥方式是先链上调用 `getSignMessage(...)` 拿 `digest`,再签名。
|
||||
- `authNonce` 必须按 `claimer` 做幂等和防重。
|
||||
- `deadline` 建议 5~30 分钟。
|
||||
|
||||
---
|
||||
|
||||
## 4. Go:ETH 后台调用 + txhash 解析事件
|
||||
|
||||
### 4.1 通过 txhash 解析 `PacketCreated/PacketClaimed/PacketRefunded`
|
||||
|
||||
```go
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
type ParsedEvent struct {
|
||||
Name string
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
func ParseEthEventsByTxHash(ctx context.Context, rpcURL, txHashHex, contractABIJSON string) ([]ParsedEvent, error) {
|
||||
cli, err := ethclient.DialContext(ctx, rpcURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
txHash := common.HexToHash(txHashHex)
|
||||
rcpt, err := cli.TransactionReceipt(ctx, txHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(contractABIJSON))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []ParsedEvent
|
||||
for _, lg := range rcpt.Logs {
|
||||
ev, ok := eventFromLog(parsedABI, lg)
|
||||
if ok {
|
||||
out = append(out, ev)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func eventFromLog(parsedABI abi.ABI, lg *types.Log) (ParsedEvent, bool) {
|
||||
if len(lg.Topics) == 0 {
|
||||
return ParsedEvent{}, false
|
||||
}
|
||||
for name, e := range parsedABI.Events {
|
||||
if e.ID != lg.Topics[0] {
|
||||
continue
|
||||
}
|
||||
vals := map[string]any{}
|
||||
|
||||
// 非 indexed 参数
|
||||
nonIndexed, err := e.Inputs.NonIndexed().Unpack(lg.Data)
|
||||
if err != nil {
|
||||
return ParsedEvent{}, false
|
||||
}
|
||||
n := 0
|
||||
idxTopic := 1
|
||||
for _, input := range e.Inputs {
|
||||
if input.Indexed {
|
||||
if idxTopic >= len(lg.Topics) {
|
||||
return ParsedEvent{}, false
|
||||
}
|
||||
vals[input.Name] = decodeIndexedTopic(input.Type, lg.Topics[idxTopic])
|
||||
idxTopic++
|
||||
} else {
|
||||
vals[input.Name] = nonIndexed[n]
|
||||
n++
|
||||
}
|
||||
}
|
||||
return ParsedEvent{Name: name, Data: vals}, true
|
||||
}
|
||||
return ParsedEvent{}, false
|
||||
}
|
||||
|
||||
func decodeIndexedTopic(t abi.Type, topic common.Hash) any {
|
||||
switch t.T {
|
||||
case abi.AddressTy:
|
||||
return common.BytesToAddress(topic.Bytes()[12:])
|
||||
default:
|
||||
return topic
|
||||
}
|
||||
}
|
||||
|
||||
func PrettyPrintEvents(events []ParsedEvent) string {
|
||||
b, _ := json.MarshalIndent(events, "", " ")
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func MustReadABIFromArtifact(artifactJSON []byte) (string, error) {
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(artifactJSON, &raw); err != nil {
|
||||
return "", err
|
||||
}
|
||||
abiObj, ok := raw["abi"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("abi field not found")
|
||||
}
|
||||
abiBytes, err := json.Marshal(abiObj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(abiBytes), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 ETH 创建/领取调用(示意)
|
||||
|
||||
建议用 `abigen` 生成 Go binding 后调用(最稳)。
|
||||
|
||||
`abigen` 示例:
|
||||
```bash
|
||||
abigen --abi abi/contracts/RedPacket.sol/RedPacket.json --pkg redpacket --type RedPacket --out redpacket_binding.go
|
||||
```
|
||||
|
||||
调用流程:
|
||||
1. `createFixedPacket/createRandomPacket/createTransfer` 发交易
|
||||
2. 拿到 `txHash` 后轮询 receipt
|
||||
3. 用上面的 `ParseEthEventsByTxHash` 解出 `PacketCreated`,拿到 `packetId`
|
||||
4. 后端签名下发给前端后,前端/后端发 `claim`
|
||||
5. 用 `PacketClaimed.amount` 作为最终到账金额
|
||||
|
||||
---
|
||||
|
||||
## 5. Go:TRON 后台调用 + txhash 解析事件
|
||||
|
||||
TRON 的 EVM 合约事件最终也是 topic/data 结构,因此事件解码可复用 EVM ABI。
|
||||
|
||||
### 5.1 通过 txhash 解析 TRON 事件(推荐走 `/wallet/gettransactioninfobyid`)
|
||||
|
||||
```go
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
type tronTxInfoResp struct {
|
||||
ID string `json:"id"`
|
||||
Log []struct {
|
||||
Address string `json:"address"` // 合约地址hex(无0x)
|
||||
Topics []string `json:"topics"` // topic hex(无0x)
|
||||
Data string `json:"data"` // data hex(无0x)
|
||||
} `json:"log"`
|
||||
}
|
||||
|
||||
func ParseTronEventsByTxHash(ctx context.Context, tronFullNodeURL, txID, contractABIJSON string) ([]ParsedEvent, error) {
|
||||
body := map[string]string{"value": txID}
|
||||
buf, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, tronFullNodeURL+"/wallet/gettransactioninfobyid", bytes.NewReader(buf))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("tron http %d: %s", resp.StatusCode, string(raw))
|
||||
}
|
||||
|
||||
var info tronTxInfoResp
|
||||
if err := json.Unmarshal(raw, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(contractABIJSON))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]ParsedEvent, 0, len(info.Log))
|
||||
for _, lg := range info.Log {
|
||||
if len(lg.Topics) == 0 {
|
||||
continue
|
||||
}
|
||||
topic0 := common.HexToHash("0x" + lg.Topics[0])
|
||||
|
||||
for name, e := range parsedABI.Events {
|
||||
if e.ID != topic0 {
|
||||
continue
|
||||
}
|
||||
vals := map[string]any{}
|
||||
|
||||
dataBytes, err := hex.DecodeString(strings.TrimPrefix(lg.Data, "0x"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonIndexed, err := e.Inputs.NonIndexed().Unpack(dataBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := 0
|
||||
idxTopic := 1
|
||||
for _, input := range e.Inputs {
|
||||
if input.Indexed {
|
||||
if idxTopic >= len(lg.Topics) {
|
||||
return nil, fmt.Errorf("missing indexed topic for event %s", name)
|
||||
}
|
||||
t := common.HexToHash("0x" + lg.Topics[idxTopic])
|
||||
vals[input.Name] = decodeIndexedTopic(input.Type, t)
|
||||
idxTopic++
|
||||
} else {
|
||||
vals[input.Name] = nonIndexed[n]
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, ParsedEvent{Name: name, Data: vals})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 TRON 后台调用流程(实践)
|
||||
|
||||
1. 组装 ABI 参数(与 ETH 一样)
|
||||
2. 调用 TRON FullNode 的 `trigger*contract` 生成未签名交易
|
||||
3. 用托管私钥签名交易并广播
|
||||
4. 根据返回 `txID` 调用上面的 `ParseTronEventsByTxHash` 解事件
|
||||
|
||||
说明:TRON 发交易接口在不同节点服务(TronGrid/自建 FullNode/SDK 封装)字段细节略有差异,建议你在项目里固定一种(推荐固定 TronGrid 或 gotron-sdk 版本),避免线上环境差异。
|
||||
|
||||
---
|
||||
|
||||
## 6. 合约参数设置(管理员)
|
||||
|
||||
需要 `CONFIG_ADMIN_ROLE` 的函数:
|
||||
- `setSigner(address signer)`
|
||||
- `setAllowAllTokens(bool allowAllTokens)`
|
||||
- `setNativeTokenEnabled(bool enabled)`
|
||||
- `setAllowedToken(address token, bool allowed, uint256 minShareAmount)`
|
||||
- `setDefaultExpiryDuration(uint256 duration)`
|
||||
|
||||
对应配置事件(可按 `txhash` 解析校验):
|
||||
- `SignerUpdated(oldSigner, newSigner)`
|
||||
- `AllowAllTokensUpdated(allowAllTokens)`
|
||||
- `NativeTokenEnabledUpdated(enabled)`
|
||||
- `AllowedTokenUpdated(token, allowed, minShareAmount)`
|
||||
- `DefaultExpiryDurationUpdated(duration)`
|
||||
|
||||
### 6.1 ETH:Go 设置合约参数(通用写法)
|
||||
|
||||
```go
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
ethereum "github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
// SendEthAdminTx 通用管理员写调用:
|
||||
// method 例如 "setNativeTokenEnabled"
|
||||
// args 对应函数参数
|
||||
func SendEthAdminTx(
|
||||
ctx context.Context,
|
||||
rpcURL string,
|
||||
contractAddr common.Address,
|
||||
priv *ecdsa.PrivateKey,
|
||||
contractABIJSON string,
|
||||
method string,
|
||||
args ...any,
|
||||
) (common.Hash, error) {
|
||||
cli, err := ethclient.DialContext(ctx, rpcURL)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
from := crypto.PubkeyToAddress(priv.PublicKey)
|
||||
nonce, err := cli.PendingNonceAt(ctx, from)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
chainID, err := cli.NetworkID(ctx)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
gasPrice, err := cli.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(contractABIJSON))
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
data, err := parsedABI.Pack(method, args...)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
msg := ethereum.CallMsg{
|
||||
From: from, To: &contractAddr, Data: data, Value: big.NewInt(0),
|
||||
}
|
||||
gasLimit, err := cli.EstimateGas(ctx, msg)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
tx := types.NewTransaction(nonce, contractAddr, big.NewInt(0), gasLimit, gasPrice, data)
|
||||
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), priv)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
if err = cli.SendTransaction(ctx, signedTx); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
return signedTx.Hash(), nil
|
||||
}
|
||||
|
||||
// 例子:开启原生币、放开所有 token、设置 token 白名单与最小份额
|
||||
func ExampleSetConfigEth(ctx context.Context, rpcURL, abiJSON, contractHex string, priv *ecdsa.PrivateKey, usdt common.Address) error {
|
||||
contract := common.HexToAddress(contractHex)
|
||||
|
||||
tx1, err := SendEthAdminTx(ctx, rpcURL, contract, priv, abiJSON, "setNativeTokenEnabled", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("setNativeTokenEnabled tx:", tx1.Hex())
|
||||
|
||||
tx2, err := SendEthAdminTx(ctx, rpcURL, contract, priv, abiJSON, "setAllowAllTokens", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("setAllowAllTokens tx:", tx2.Hex())
|
||||
|
||||
tx3, err := SendEthAdminTx(ctx, rpcURL, contract, priv, abiJSON, "setAllowedToken", usdt, true, big.NewInt(1_000_000))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("setAllowedToken tx:", tx3.Hex())
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
注意:`setAllowedToken(..., minShareAmount)` 的单位是 token 最小单位(例如 6 位精度 token,`1_000_000` 代表 1 个 token)。
|
||||
|
||||
### 6.2 TRON:Go 设置合约参数(FullNode HTTP)
|
||||
|
||||
TRON 推荐流程:`triggersmartcontract -> gettransactionsign -> broadcasttransaction`。
|
||||
|
||||
```go
|
||||
package redpacket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
)
|
||||
|
||||
func encodeTronParams(abiJSON, method string, args ...any) (string, error) {
|
||||
parsed, err := abi.JSON(strings.NewReader(abiJSON))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m, ok := parsed.Methods[method]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("method not found: %s", method)
|
||||
}
|
||||
packed, err := m.Inputs.Pack(args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(packed), nil
|
||||
}
|
||||
|
||||
func postJSON(ctx context.Context, url string, body any, out any) error {
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("http %d: %s", resp.StatusCode, string(raw))
|
||||
}
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendTronAdminTx 示例:
|
||||
// selector 例子 "setNativeTokenEnabled(bool)"
|
||||
// methodName 例子 "setNativeTokenEnabled"
|
||||
func SendTronAdminTx(
|
||||
ctx context.Context,
|
||||
fullNodeURL, ownerBase58, contractBase58, selector, methodName string,
|
||||
feeLimit int64,
|
||||
privateKeyHex string,
|
||||
abiJSON string,
|
||||
args ...any,
|
||||
) (string, error) {
|
||||
paramHex, err := encodeTronParams(abiJSON, methodName, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var triggerResp map[string]any
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/triggersmartcontract", map[string]any{
|
||||
"owner_address": ownerBase58,
|
||||
"contract_address": contractBase58,
|
||||
"function_selector": selector,
|
||||
"parameter": paramHex,
|
||||
"fee_limit": feeLimit,
|
||||
"call_value": 0,
|
||||
"visible": true,
|
||||
}, &triggerResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
txObj, ok := triggerResp["transaction"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("transaction not found in trigger response")
|
||||
}
|
||||
|
||||
var signedResp map[string]any
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/gettransactionsign", map[string]any{
|
||||
"transaction": txObj,
|
||||
"privateKey": privateKeyHex,
|
||||
}, &signedResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var broadcastResp map[string]any
|
||||
err = postJSON(ctx, fullNodeURL+"/wallet/broadcasttransaction", signedResp, &broadcastResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if result, _ := broadcastResp["result"].(bool); !result {
|
||||
return "", fmt.Errorf("broadcast failed: %v", broadcastResp)
|
||||
}
|
||||
|
||||
txid, _ := broadcastResp["txid"].(string)
|
||||
return txid, nil
|
||||
}
|
||||
```
|
||||
|
||||
调用示例:
|
||||
- `setNativeTokenEnabled(true)`:
|
||||
`selector = "setNativeTokenEnabled(bool)"`,`methodName = "setNativeTokenEnabled"`,`args = true`
|
||||
- `setAllowAllTokens(false)`:
|
||||
`selector = "setAllowAllTokens(bool)"`,`methodName = "setAllowAllTokens"`,`args = false`
|
||||
- `setAllowedToken(token, true, 1_000_000)`:
|
||||
`selector = "setAllowedToken(address,bool,uint256)"`,`methodName = "setAllowedToken"`,`args = common.HexToAddress(tokenHexAddress), true, big.NewInt(1_000_000)`
|
||||
|
||||
安全建议:生产环境不要把私钥直接传给节点接口,建议改为本地离线签名或托管签名服务。
|
||||
|
||||
---
|
||||
|
||||
## 7. 最小落地建议(直接可用)
|
||||
|
||||
- 统一保存:`chain + txHash + packetId + eventName + rawEventJson`
|
||||
- 创建成功:只认 `PacketCreated.packetId`
|
||||
- 领取成功:只认 `PacketClaimed.amount`
|
||||
- 退款成功:只认 `PacketRefunded.amount`
|
||||
- 签名服务:`authNonce` 做地址维度去重;`deadline` 过期即废弃
|
||||
@ -0,0 +1,751 @@
|
||||
# RedPacket Web3 接入设计文档
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档用于指导 `RedPacket` 红包系统的 Web3 接入落地,覆盖:
|
||||
|
||||
- 整体架构设计
|
||||
- 前端 / 钱包 / 后端 / 合约 / 监听服务 的职责划分
|
||||
- 初始化与配置流程
|
||||
- 创建红包流程
|
||||
- 领取红包流程
|
||||
- 退款流程
|
||||
- 关键接口定义
|
||||
- 关键数据流与安全边界
|
||||
|
||||
本文档基于当前 `RedPacket` 合约规则整理:
|
||||
|
||||
- 链上 `packetId` 由合约自增生成,创建成功后通过 `PacketCreated` 事件回传。fileciteturn1file1
|
||||
- `claim` 必须携带后端签名,签名消息绑定 `packetId + claimer + authNonce + randomSeed + deadline`,并与 `msg.sender` 强绑定。fileciteturn1file1
|
||||
- `createTransfer` 创建时不传 `recipient`,实际可领取人由后端签名中的 `claimer` 决定。fileciteturn1file1
|
||||
- 建议后端通过 `getSignMessage(...)` 获取 digest 后做裸签名,避免 `signMessage` 前缀导致链上验签失败。fileciteturn1file4
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
### 2.1 业务目标
|
||||
|
||||
支持以下红包能力:
|
||||
|
||||
- 普通红包(固定金额)
|
||||
- 拼手气红包(随机金额)
|
||||
- 待领取转账(创建时不传接收地址,领取时由后端鉴权)fileciteturn1file1
|
||||
|
||||
### 2.2 安全目标
|
||||
|
||||
系统需明确区分两类链上信任地址:
|
||||
|
||||
1. **参数配置地址(configAdmin)**
|
||||
- 用于调用配置类函数
|
||||
- 例如:`setSigner`、`setAllowAllTokens`、`setNativeTokenEnabled`、`setAllowedToken`、`setDefaultExpiryDuration`。fileciteturn1file4
|
||||
|
||||
2. **业务签名地址(signer)**
|
||||
- 用于后端签发领取授权
|
||||
- 合约 `claim` 时通过验签校验是否为可信签名地址
|
||||
|
||||
### 2.3 工程目标
|
||||
|
||||
- 前端只负责钱包连接、读链、发交易、展示状态
|
||||
- 后端负责业务鉴权、nonce 管理、签名发放、审计落库
|
||||
- 合约负责最终状态机、权限控制、验签、防重放
|
||||
- 监听服务负责链上事件消费、对账与最终一致性
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
## 3.1 架构图
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[用户] --> FE[前端 / H5 / App]
|
||||
FE --> W[钱包 Wallet]
|
||||
FE --> BE[业务后端 API]
|
||||
|
||||
BE --> AuthSvc[签名服务\n持有 signer 私钥]
|
||||
BE --> AdminSvc[配置服务\n持有 configAdmin 私钥]
|
||||
BE --> DB[(业务库 / 审计库)]
|
||||
|
||||
W --> RP[RedPacket 合约]
|
||||
AuthSvc --> RP
|
||||
AdminSvc --> RP
|
||||
|
||||
RP --> Indexer[链监听 / 索引服务]
|
||||
Indexer --> DB
|
||||
```
|
||||
|
||||
## 3.2 模块职责
|
||||
|
||||
### 前端 / H5 / App
|
||||
|
||||
负责:
|
||||
|
||||
- 连接钱包
|
||||
- 获取当前链地址
|
||||
- 读取红包状态
|
||||
- 创建红包前预校验
|
||||
- 发起创建交易
|
||||
- 调后端获取领取签名
|
||||
- 发起领取交易
|
||||
- 解析交易回执与事件
|
||||
|
||||
### 钱包
|
||||
|
||||
负责:
|
||||
|
||||
- 用户签名交易
|
||||
- 广播创建 / 领取 / 退款交易
|
||||
- 提供当前地址与网络信息
|
||||
|
||||
### 业务后端 API
|
||||
|
||||
负责:
|
||||
|
||||
- 业务单管理
|
||||
- 创建结果落库
|
||||
- 领取资格鉴权
|
||||
- 签名发放接口
|
||||
- 配置管理接口
|
||||
- 审计与风控
|
||||
|
||||
### 签名服务
|
||||
|
||||
负责:
|
||||
|
||||
- 使用 `signer` 私钥对领取摘要做裸签名
|
||||
- 不参与链上参数修改
|
||||
- 不应持有配置类权限
|
||||
|
||||
### 配置服务
|
||||
|
||||
负责:
|
||||
|
||||
- 使用 `configAdmin` 私钥调用配置类交易
|
||||
- 负责 `signer` 轮换与 token 配置变更
|
||||
- 不参与高频 claim 签名发放
|
||||
|
||||
### RedPacket 合约
|
||||
|
||||
负责:
|
||||
|
||||
- 红包状态管理
|
||||
- 红包 ID 自增
|
||||
- 创建 / 领取 / 退款规则执行
|
||||
- claim 验签
|
||||
- nonce 防重放
|
||||
- 事件输出
|
||||
|
||||
### 链监听 / 索引服务
|
||||
|
||||
负责:
|
||||
|
||||
- 监听 `PacketCreated / PacketClaimed / PacketRefunded`
|
||||
- 解析事件并更新数据库
|
||||
- 做对账与最终一致性
|
||||
|
||||
---
|
||||
|
||||
## 4. 合约角色与权限模型
|
||||
|
||||
## 4.1 推荐角色
|
||||
|
||||
建议合约维护以下 3 类地址:
|
||||
|
||||
- `owner`:最高权限,建议多签控制
|
||||
- `configAdmin`:参数配置地址
|
||||
- `signer`:后端业务签名地址
|
||||
|
||||
## 4.2 权限建议
|
||||
|
||||
| 角色 | 用途 | 是否高频 | 建议托管方式 |
|
||||
|---|---|---:|---|
|
||||
| `owner` | 设置 `configAdmin`、兜底治理 | 否 | 多签 / 冷钱包 |
|
||||
| `configAdmin` | 修改 `signer`、token 配置、默认过期时间 | 低频 | KMS / HSM / 运维专用钱包 |
|
||||
| `signer` | 签发 claim 授权 | 高频 | 独立签名服务 |
|
||||
|
||||
## 4.3 合约与服务端的鉴权方式
|
||||
|
||||
链上无法识别“某个后端进程”,只能识别两种身份:
|
||||
|
||||
1. **交易发送者地址**
|
||||
- 用于配置类操作
|
||||
- 通过 `msg.sender` 校验
|
||||
|
||||
2. **消息签名者地址**
|
||||
- 用于领取授权
|
||||
- 通过 `ECDSA.recover(signature)` 校验
|
||||
|
||||
因此:
|
||||
|
||||
- 配置鉴权依赖 `msg.sender == configAdmin` 或 `owner`
|
||||
- claim 鉴权依赖 `recover(signature) == signer`
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键业务规则
|
||||
|
||||
### 5.1 红包 ID 规则
|
||||
|
||||
- 链上红包 ID 由 `nextPacketId` 自增生成。fileciteturn1file1
|
||||
- 前端和后端都不能自己猜 `packetId`。
|
||||
- 创建成功后必须从 `PacketCreated` 事件中解析 `packetId`。fileciteturn1file0
|
||||
|
||||
### 5.2 待领取转账规则
|
||||
|
||||
- `createTransfer` 不接收 `recipient` 参数。fileciteturn1file1
|
||||
- 实际领取人由后端签名中的 `claimer` 决定。fileciteturn1file1
|
||||
|
||||
### 5.3 claim 鉴权规则
|
||||
|
||||
`claim` 必须携带后端签名,签名字段绑定:
|
||||
|
||||
- `packetId`
|
||||
- `claimer`
|
||||
- `authNonce`
|
||||
- `randomSeed`
|
||||
- `deadline` fileciteturn1file1
|
||||
|
||||
并且签名应与 `msg.sender` 强绑定,不能被其他地址复用。fileciteturn1file1
|
||||
|
||||
### 5.4 过期规则
|
||||
|
||||
- 红包过期后不可继续领取。fileciteturn1file1
|
||||
- 过期后可调用 `refund(packetId)` 退回剩余金额。fileciteturn1file1
|
||||
- 允许创建人或管理员调用退款。fileciteturn1file4
|
||||
|
||||
### 5.5 最小份额规则
|
||||
|
||||
不同 token 可在 `setAllowedToken(token, allowed, minShareAmount)` 中配置最小份额。fileciteturn1file1
|
||||
|
||||
创建校验:
|
||||
|
||||
- 固定红包:`totalAmount / totalShares >= minShareAmount`
|
||||
- 拼手气红包:`totalAmount >= totalShares * minShareAmount`
|
||||
- 转账:`amount >= minShareAmount` fileciteturn1file1
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键交互时序图
|
||||
|
||||
## 6.1 初始化与配置流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Owner as Owner/多签
|
||||
participant ConfigSvc as 配置服务(configAdmin私钥)
|
||||
participant RP as RedPacket合约
|
||||
participant DB as 审计库
|
||||
|
||||
Owner->>RP: setConfigAdmin(configAdminAddress)
|
||||
RP-->>Owner: tx success
|
||||
|
||||
ConfigSvc->>RP: setSigner(signerAddress)
|
||||
RP-->>ConfigSvc: tx success
|
||||
|
||||
ConfigSvc->>RP: setAllowAllTokens(...)
|
||||
RP-->>ConfigSvc: tx success
|
||||
|
||||
ConfigSvc->>RP: setNativeTokenEnabled(...)
|
||||
RP-->>ConfigSvc: tx success
|
||||
|
||||
ConfigSvc->>RP: setAllowedToken(token, allowed, minShareAmount)
|
||||
RP-->>ConfigSvc: tx success
|
||||
|
||||
ConfigSvc->>RP: setDefaultExpiryDuration(duration)
|
||||
RP-->>ConfigSvc: tx success
|
||||
|
||||
ConfigSvc->>DB: 记录配置变更审计
|
||||
```
|
||||
|
||||
### 图意概述
|
||||
|
||||
该流程用于完成合约上线后的初始参数配置与权限分层。`owner` 负责设置 `configAdmin`,而日常配置由 `configAdmin` 地址发起。`signer` 地址由配置服务设置,用于后续领取签名验证。
|
||||
|
||||
### 边界条件
|
||||
|
||||
- `signer` 与 `configAdmin` 必须是不同地址,避免签名服务被攻破后直接具备配置权限。
|
||||
- `owner` 建议使用多签地址,不建议使用个人热钱包。
|
||||
- 所有配置写操作都应带链上事件与业务侧审计单。
|
||||
|
||||
### 异常路径与回退
|
||||
|
||||
- 如果配置交易失败,前端/后台应展示链上 revert 原因。
|
||||
- 如果设置 `signer` 失败,旧 `signer` 应继续有效,避免线上 claim 全量失败。
|
||||
- 如果 token 配置更新失败,前端仍应以链上真实配置为准。
|
||||
|
||||
### 性能与容量假设
|
||||
|
||||
- 配置操作为低频操作,可接受链上确认延迟。
|
||||
- 配置写入频率极低,因此可优先保障安全性而非吞吐。
|
||||
|
||||
### 版本与兼容性
|
||||
|
||||
- 若后续扩展角色(如 `pauser` / `upgrader`),建议继续沿用分权设计。
|
||||
- 配置事件建议保持向后兼容,便于监听服务稳定消费。
|
||||
|
||||
---
|
||||
|
||||
## 6.2 创建红包流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as 用户
|
||||
participant FE as 前端
|
||||
participant Wallet as 钱包
|
||||
participant RP as RedPacket合约
|
||||
participant BE as 业务后端
|
||||
participant DB as 业务库
|
||||
participant Indexer as 链监听服务
|
||||
|
||||
U->>FE: 打开创建红包页面
|
||||
FE->>RP: getAllTokenConfigs()
|
||||
RP-->>FE: token配置
|
||||
|
||||
U->>FE: 输入金额/份数/过期时间/类型
|
||||
FE->>RP: getCreateValidation(token, packetType, totalAmount, totalShares)
|
||||
RP-->>FE: 校验结果
|
||||
|
||||
alt 校验失败
|
||||
FE-->>U: 提示失败原因
|
||||
else 校验通过
|
||||
FE->>Wallet: 检查余额/allowance
|
||||
Wallet-->>FE: 返回余额/授权状态
|
||||
|
||||
FE->>RP: staticCall(createXXX(...))
|
||||
RP-->>FE: 模拟通过
|
||||
|
||||
FE->>Wallet: 发起 createXXX 交易
|
||||
Wallet->>RP: createFixedPacket/createRandomPacket/createTransfer
|
||||
RP-->>Wallet: tx hash
|
||||
Wallet-->>FE: tx hash
|
||||
|
||||
FE->>Wallet: wait receipt
|
||||
Wallet-->>FE: receipt + logs
|
||||
|
||||
FE->>FE: 解析 PacketCreated
|
||||
FE->>FE: 得到 packetId
|
||||
|
||||
FE->>BE: created-callback(bizId, txHash, packetId)
|
||||
BE->>DB: 保存 bizId <-> txHash <-> packetId
|
||||
|
||||
Indexer->>RP: 监听 PacketCreated
|
||||
RP-->>Indexer: PacketCreated(...)
|
||||
Indexer->>DB: 对账/补写
|
||||
|
||||
FE-->>U: 展示创建成功
|
||||
end
|
||||
```
|
||||
|
||||
### 图意概述
|
||||
|
||||
创建红包流程分为:读配置、权威校验、余额与授权检查、链上模拟、正式创建、事件解析、后端落库。`packetId` 的唯一可信来源是 `PacketCreated` 事件。fileciteturn1file0
|
||||
|
||||
### 边界条件
|
||||
|
||||
- 原生币需额外预留 gas,不应把余额全部作为 `totalAmount`。
|
||||
- ERC20 创建前需检查 `allowance >= totalAmount`。
|
||||
- `expiryAt == 0` 时由合约使用默认过期时间。fileciteturn1file4
|
||||
|
||||
### 异常路径与回退
|
||||
|
||||
- `getCreateValidation(...)` 返回 `passed == false` 时,应直接用 `code` 透传失败原因。fileciteturn1file3
|
||||
- `staticCall` 成功并不保证正式交易 100% 成功,链上配置变化、余额变化都可能导致最终失败。fileciteturn1file3
|
||||
- 若前端回传 `packetId` 失败,可由监听服务通过 `txHash` 和事件补写。
|
||||
|
||||
### 性能与容量假设
|
||||
|
||||
- 创建链路以用户交互为主,整体延迟由钱包签名和链确认决定。
|
||||
- `getAllTokenConfigs()` 适合页面初始化时缓存,减少重复读链。fileciteturn1file3
|
||||
|
||||
### 版本与兼容性
|
||||
|
||||
- 创建页应优先依赖聚合只读接口,避免未来 token 规则变化导致前端多处改动。
|
||||
- 若未来扩展红包类型,建议继续复用 `getCreateValidation(...)` 做统一校验出口。
|
||||
|
||||
---
|
||||
|
||||
## 6.3 领取红包流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as 用户
|
||||
participant FE as 前端
|
||||
participant Wallet as 钱包
|
||||
participant BE as 业务后端
|
||||
participant AuthSvc as 签名服务(signer私钥)
|
||||
participant DB as 业务库
|
||||
participant RP as RedPacket合约
|
||||
participant Indexer as 链监听服务
|
||||
|
||||
U->>FE: 打开红包详情页
|
||||
FE->>Wallet: 获取当前地址
|
||||
Wallet-->>FE: userAddress
|
||||
|
||||
FE->>RP: getPacketInfoForUser(packetId, userAddress)
|
||||
RP-->>FE: packet/status/alreadyClaimed/canClaimByChain
|
||||
|
||||
alt 链上预判不可领取
|
||||
FE-->>U: 展示不可领取状态
|
||||
else 可领取
|
||||
U->>FE: 点击领取
|
||||
FE->>BE: POST /claim-sign(packetId, claimer, randomSeed)
|
||||
|
||||
BE->>DB: 查询业务资格/业务单
|
||||
DB-->>BE: 返回业务状态
|
||||
|
||||
alt 鉴权失败
|
||||
BE-->>FE: 拒绝签名
|
||||
FE-->>U: 提示无资格领取
|
||||
else 鉴权通过
|
||||
BE->>DB: 生成 authNonce
|
||||
DB-->>BE: authNonce
|
||||
|
||||
BE->>RP: getSignMessage(packetId, claimer, authNonce, randomSeed, deadline)
|
||||
RP-->>BE: digest
|
||||
|
||||
BE->>AuthSvc: 使用 signer 私钥裸签 digest
|
||||
AuthSvc-->>BE: signature
|
||||
|
||||
BE->>DB: 保存签名发放记录
|
||||
BE-->>FE: authNonce + randomSeed + deadline + signature
|
||||
|
||||
FE->>Wallet: 调用 claim(packetId, authNonce, randomSeed, deadline, signature)
|
||||
Wallet->>RP: 发起 claim
|
||||
|
||||
RP->>RP: 校验 packet 状态
|
||||
RP->>RP: 校验 alreadyClaimed == false
|
||||
RP->>RP: 校验 authNonce 未使用
|
||||
RP->>RP: recover(signature) == signer
|
||||
RP->>RP: 计算领取金额
|
||||
RP->>RP: 更新红包剩余状态
|
||||
RP-->>Wallet: tx hash
|
||||
|
||||
Wallet-->>FE: tx hash
|
||||
FE->>Wallet: wait receipt
|
||||
Wallet-->>FE: receipt + logs
|
||||
|
||||
FE->>FE: 解析 PacketClaimed.amount
|
||||
FE-->>U: 展示领取成功与实际金额
|
||||
|
||||
Indexer->>RP: 监听 PacketClaimed
|
||||
RP-->>Indexer: PacketClaimed(...)
|
||||
Indexer->>DB: 更新领取记录/红包状态
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 图意概述
|
||||
|
||||
领取链路是整个系统最核心的链路。前端只能做链上预判,最终是否允许领取由后端业务鉴权 + 后端签名 + 合约验签三者共同决定。`claim` 不是纯前端直连模式,而是“前端 + 后端签名服务 + 合约”三方联动。fileciteturn1file1
|
||||
|
||||
### 边界条件
|
||||
|
||||
- `authNonce` 必须对每个 `claimer` 唯一,不可重复。fileciteturn1file4
|
||||
- `deadline` 建议短时有效,如 5~30 分钟。fileciteturn1file4
|
||||
- `claimer` 应严格使用当前连接钱包地址,避免签给 A 地址却由 B 地址调用。
|
||||
- 拼手气红包最终领取金额必须以链上 `PacketClaimed.amount` 为准,前端不要本地复算。fileciteturn1file2
|
||||
|
||||
### 异常路径与回退
|
||||
|
||||
- 后端鉴权失败:直接拒绝签名。
|
||||
- `invalid signature`:签名人错误、参数不一致、`claimer` 被篡改、摘要计算不一致。fileciteturn1file1
|
||||
- `claim nonce used`:同地址重复使用 `authNonce`。fileciteturn1file1
|
||||
- `packet expired`:红包过期。fileciteturn1file1
|
||||
|
||||
### 性能与容量假设
|
||||
|
||||
- claim 为高频路径,签名服务应尽量轻量,避免承担复杂配置职责。
|
||||
- 建议签名接口短链路完成,仅依赖必要的业务状态查询与 nonce 生成。
|
||||
- 监听服务需具备幂等更新能力,防止事件重复消费。
|
||||
|
||||
### 版本与兼容性
|
||||
|
||||
- 若签名结构变更,应同步升级合约 `CLAIM_TYPEHASH`、后端签名逻辑与前端参数组装。
|
||||
- 若未来切换 signer 地址,保留 `setSigner(...)` 即可平滑轮换。fileciteturn1file4
|
||||
|
||||
---
|
||||
|
||||
## 6.4 过期退款流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as 创建人/管理员
|
||||
participant FE as 前端/后台
|
||||
participant Wallet as 钱包
|
||||
participant RP as RedPacket合约
|
||||
participant Indexer as 链监听服务
|
||||
participant DB as 业务库
|
||||
|
||||
U->>FE: 点击退款
|
||||
FE->>RP: 查询 packet 状态
|
||||
RP-->>FE: creator/expiryAt/refunded/remainingAmount
|
||||
|
||||
alt 不可退款
|
||||
FE-->>U: 提示失败
|
||||
else 可退款
|
||||
FE->>Wallet: 调用 refund(packetId)
|
||||
Wallet->>RP: 发起退款交易
|
||||
|
||||
RP->>RP: 校验 packet 存在
|
||||
RP->>RP: 校验已过期
|
||||
RP->>RP: 校验调用方是创建人或管理员
|
||||
RP->>RP: 退还 remainingAmount
|
||||
RP->>RP: 标记 refunded = true
|
||||
RP-->>Wallet: tx hash
|
||||
|
||||
Wallet-->>FE: tx hash
|
||||
FE-->>U: 提示退款已提交
|
||||
|
||||
Indexer->>RP: 监听 PacketRefunded
|
||||
RP-->>Indexer: PacketRefunded(...)
|
||||
Indexer->>DB: 更新状态为 REFUNDED
|
||||
end
|
||||
```
|
||||
|
||||
### 图意概述
|
||||
|
||||
退款链路只允许在红包过期后执行,且调用方必须是创建人或管理员。成功后需通过 `PacketRefunded` 事件更新业务状态。fileciteturn1file4
|
||||
|
||||
### 边界条件
|
||||
|
||||
- 退款前必须确认 `refunded == false`。
|
||||
- 已领取完的红包理论上剩余金额为 0,退款交易应仍保持一致性处理。
|
||||
- 管理员退款与创建人退款都应有审计落库。
|
||||
|
||||
### 异常路径与回退
|
||||
|
||||
- 未过期调用应直接 revert。
|
||||
- 非创建人/管理员调用应直接拒绝。
|
||||
- 如果退款交易已提交但后端未更新,可由监听服务补偿。
|
||||
|
||||
### 性能与容量假设
|
||||
|
||||
- 退款为低频操作,对吞吐要求低。
|
||||
- 事件驱动回写可以接受秒级到分钟级延迟。
|
||||
|
||||
### 版本与兼容性
|
||||
|
||||
- 若未来增加自动退款策略,可在不改变 `refund(packetId)` 主接口的前提下扩展调度能力。
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键接口表
|
||||
|
||||
## 7.1 合约接口表
|
||||
|
||||
| 分类 | 接口 | 参数 | 返回 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| 创建 | `createFixedPacket` | `token, totalAmount, totalShares, expiryAt` | `packetId` / tx receipt | 创建固定金额红包。fileciteturn1file4 |
|
||||
| 创建 | `createRandomPacket` | `token, totalAmount, totalShares, expiryAt` | `packetId` / tx receipt | 创建拼手气红包。fileciteturn1file4 |
|
||||
| 创建 | `createTransfer` | `token, amount, expiryAt` | `packetId` / tx receipt | 创建待领取转账,不传 recipient。fileciteturn1file1turn1file4 |
|
||||
| 领取 | `claim` | `packetId, authNonce, randomSeed, deadline, signature` | tx receipt | 必须携带后端签名。fileciteturn1file1turn1file4 |
|
||||
| 退款 | `refund` | `packetId` | tx receipt | 红包过期后退款。fileciteturn1file4 |
|
||||
| 管理 | `setSigner` | `signer` | tx receipt | 设置验签地址。fileciteturn1file4 |
|
||||
| 管理 | `setAllowAllTokens` | `allowAllTokens` | tx receipt | 设置是否允许所有 token。fileciteturn1file4 |
|
||||
| 管理 | `setNativeTokenEnabled` | `enabled` | tx receipt | 设置原生币开关。fileciteturn1file4 |
|
||||
| 管理 | `setAllowedToken` | `token, allowed, minShareAmount` | tx receipt | 设置 token 白名单与最小份额。fileciteturn1file1turn1file4 |
|
||||
| 管理 | `setDefaultExpiryDuration` | `duration` | tx receipt | 设置默认过期时间。fileciteturn1file4 |
|
||||
| 只读 | `getSignMessage` | `packetId, claimer, authNonce, randomSeed, deadline` | `bytes32 digest` | 后端获取摘要再裸签名。fileciteturn1file4 |
|
||||
| 只读 | `getPacketInfoForUser` | `packetId, user` | `packet, status, alreadyClaimed, canClaimByChain` | 前端聚合查询红包状态。fileciteturn1file3 |
|
||||
| 只读 | `getAllTokenConfigs` | - | token 配置聚合结果 | 页面初始化时获取 token 配置。fileciteturn1file3 |
|
||||
| 只读 | `getCreateValidation` | `token, packetType, totalAmount, totalShares` | `passed/code/...` | 创建前权威校验。fileciteturn1file3 |
|
||||
|
||||
## 7.2 后端 API 接口表
|
||||
|
||||
| 分类 | 接口 | 方法 | 关键入参 | 关键出参 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| 创建 | `/api/redpacket/create-order` | `POST` | 业务发红包参数 | `bizId` | 创建业务单,链前预落库 |
|
||||
| 创建回写 | `/api/redpacket/created-callback` | `POST` | `bizId, txHash, packetId` | `ok` | 创建交易成功后回写链上 `packetId` |
|
||||
| 详情 | `/api/redpacket/detail` | `GET` | `packetId` | 红包业务详情 | 返回分享页需要的业务信息 |
|
||||
| 领取签名 | `/api/redpacket/claim-sign` | `POST` | `packetId, claimer, randomSeed` | `authNonce, deadline, signature` | 业务鉴权 + 发放 claim 授权 |
|
||||
| 领取回写 | `/api/redpacket/claim-result` | `POST` | `packetId, txHash` | `ok` | 可选,最终仍以监听服务为准 |
|
||||
| 配置 | `/admin/redpacket/set-signer` | `POST` | `newSigner` | `txHash` | 变更 signer |
|
||||
| 配置 | `/admin/redpacket/set-token` | `POST` | `token, allowed, minShareAmount` | `txHash` | 更新 token 配置 |
|
||||
| 配置 | `/admin/redpacket/set-expiry` | `POST` | `duration` | `txHash` | 更新默认过期时间 |
|
||||
|
||||
## 7.3 事件表
|
||||
|
||||
| 事件 | 字段 | 用途 |
|
||||
|---|---|---|
|
||||
| `PacketCreated` | `packetId, creator, packetType, token, totalAmount, totalShares, expiryAt` | 创建成功后的唯一 `packetId` 来源。fileciteturn1file0turn1file1 |
|
||||
| `PacketClaimed` | `packetId, claimer, amount, remainingAmount, remainingShares, authNonce` | 领取成功与实际领取金额来源。fileciteturn1file2 |
|
||||
| `PacketRefunded` | `packetId, operator, refundTo, amount` | 退款确认与状态同步。fileciteturn1file4 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 关键数据表建议
|
||||
|
||||
## 8.1 红包主表 `red_packet`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `id` | 自增主键 |
|
||||
| `biz_id` | 业务单号 |
|
||||
| `packet_id` | 链上红包 ID |
|
||||
| `chain_id` | 链 ID |
|
||||
| `contract_address` | 合约地址 |
|
||||
| `creator_user_id` | 发红包业务用户 ID |
|
||||
| `creator_wallet` | 发红包钱包地址 |
|
||||
| `packet_type` | 红包类型 |
|
||||
| `token` | token 地址 |
|
||||
| `total_amount` | 总金额 |
|
||||
| `total_shares` | 总份数 |
|
||||
| `expiry_at` | 过期时间 |
|
||||
| `tx_hash` | 创建交易哈希 |
|
||||
| `status` | 业务状态 |
|
||||
| `created_at` | 创建时间 |
|
||||
|
||||
## 8.2 领取授权表 `red_packet_claim_auth`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `id` | 主键 |
|
||||
| `packet_id` | 红包 ID |
|
||||
| `claimer_wallet` | 领取地址 |
|
||||
| `auth_nonce` | 授权 nonce |
|
||||
| `random_seed` | 随机参数 |
|
||||
| `deadline` | 过期时间 |
|
||||
| `signature` | 后端签名 |
|
||||
| `used` | 是否已使用 |
|
||||
| `user_id` | 业务用户 ID |
|
||||
| `created_at` | 创建时间 |
|
||||
|
||||
## 8.3 领取记录表 `red_packet_claim`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `id` | 主键 |
|
||||
| `packet_id` | 红包 ID |
|
||||
| `claimer_wallet` | 领取地址 |
|
||||
| `auth_nonce` | 使用的 nonce |
|
||||
| `claim_tx_hash` | 领取交易哈希 |
|
||||
| `claimed_amount` | 实际领取金额 |
|
||||
| `block_number` | 区块号 |
|
||||
| `status` | 状态 |
|
||||
| `created_at` | 创建时间 |
|
||||
|
||||
## 8.4 退款记录表 `red_packet_refund`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `id` | 主键 |
|
||||
| `packet_id` | 红包 ID |
|
||||
| `refund_tx_hash` | 退款交易哈希 |
|
||||
| `refund_to` | 退款目标地址 |
|
||||
| `amount` | 退款金额 |
|
||||
| `status` | 状态 |
|
||||
| `created_at` | 创建时间 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 前端接入建议
|
||||
|
||||
## 9.1 创建页
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. 调 `getAllTokenConfigs()` 初始化页面配置。fileciteturn1file3
|
||||
2. 用户输入金额/份数后调 `getCreateValidation(...)`。fileciteturn1file3
|
||||
3. 检查余额 / allowance。
|
||||
4. 调 `staticCall` 做链上模拟。fileciteturn1file3
|
||||
5. 发创建交易。
|
||||
6. 从 `PacketCreated` 解析 `packetId`。fileciteturn1file0
|
||||
7. 回传后端落库。
|
||||
|
||||
## 9.2 详情页 / 领取页
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. 调 `getPacketInfoForUser(packetId, userAddress)`。fileciteturn1file3
|
||||
2. 若链上预判可领,展示领取按钮。
|
||||
3. 点击领取后先调后端 `/claim-sign`。
|
||||
4. 拿到 `authNonce + deadline + signature` 后再发 `claim(...)`。
|
||||
5. 从 `PacketClaimed.amount` 获取真实领取金额。fileciteturn1file2
|
||||
|
||||
---
|
||||
|
||||
## 10. 安全设计建议
|
||||
|
||||
## 10.1 分权
|
||||
|
||||
必须分离:
|
||||
|
||||
- `configAdmin` 私钥
|
||||
- `signer` 私钥
|
||||
|
||||
不要使用同一把私钥同时做:
|
||||
|
||||
- 配置交易
|
||||
- claim 签名
|
||||
|
||||
## 10.2 防重放
|
||||
|
||||
- `authNonce` 必须唯一,建议按 `claimer` 维度发号。fileciteturn1file4
|
||||
- claim 成功后链上立即标记 nonce 已使用。
|
||||
|
||||
## 10.3 签名规范
|
||||
|
||||
- 推荐通过 `getSignMessage(...)` 获取 digest。fileciteturn1file4
|
||||
- 后端对 digest 做裸签名。
|
||||
- 不要使用 `signMessage`,否则会添加前缀导致验签失败。fileciteturn1file4
|
||||
|
||||
## 10.4 审计与对账
|
||||
|
||||
- 所有配置变更写审计单
|
||||
- 所有签名发放写记录
|
||||
- 所有链上事件由监听服务落最终状态
|
||||
- `txHash -> packetId`、`packetId -> claim records` 都要可追溯。fileciteturn1file0
|
||||
|
||||
---
|
||||
|
||||
## 11. 常见失败原因
|
||||
|
||||
| 错误 | 含义 |
|
||||
|---|---|
|
||||
| `invalid signature` | 签名不匹配、签名人错误、claimer 不匹配、参数被篡改。fileciteturn1file1 |
|
||||
| `claim nonce used` | 同地址重复使用授权 nonce。fileciteturn1file1 |
|
||||
| `packet expired` | 红包已过期。fileciteturn1file1 |
|
||||
| `random packet amount too small` | 拼手气总额不满足最小份额。fileciteturn1file1 |
|
||||
| `fixed packet amount too small` | 固定红包单份金额小于最小份额。fileciteturn1file1 |
|
||||
| `transfer amount too small` | 转账金额小于最小份额。fileciteturn1file1 |
|
||||
| `token not allowed` | token 未开放或被禁用。fileciteturn1file3 |
|
||||
| `native token disabled` | 原生币红包未开放。fileciteturn1file3 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 落地建议
|
||||
|
||||
推荐按以下顺序推进:
|
||||
|
||||
1. **先完成合约分权改造**
|
||||
- 增加 `configAdmin`
|
||||
- 保留 `setSigner`
|
||||
- claim 使用 `signer` 验签
|
||||
|
||||
2. **再完成后端两类服务拆分**
|
||||
- 配置服务
|
||||
- 签名服务
|
||||
|
||||
3. **再接前端创建与领取流程**
|
||||
- 创建页
|
||||
- 红包详情页
|
||||
- claim 签名获取接口
|
||||
|
||||
4. **最后补监听与审计**
|
||||
- 事件消费
|
||||
- 对账补偿
|
||||
- 配置与签名审计
|
||||
|
||||
---
|
||||
|
||||
## 13. 一句话总结
|
||||
|
||||
这套红包 Web3 接入的核心不是“前端直接调合约”,而是:
|
||||
|
||||
> **前端负责发交易与展示,后端负责业务鉴权与签名发放,合约负责最终状态机与验签,监听服务负责最终一致性。**
|
||||
|
||||
BIN
cmd/openim-rpc/openim-rpc-redpacket/redpacket.db
Normal file
BIN
cmd/openim-rpc/openim-rpc-redpacket/redpacket.db
Normal file
Binary file not shown.
34
cmd/openim-rpc/openim-rpc-redpacket/router/router.go
Normal file
34
cmd/openim-rpc/openim-rpc-redpacket/router/router.go
Normal file
@ -0,0 +1,34 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"redpacket/internal/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Setup(r *gin.Engine, rpHandler *handler.RedPacketHandler, adminHandler *handler.AdminHandler) {
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// User-facing red packet APIs
|
||||
api := r.Group("/api/redpacket")
|
||||
{
|
||||
api.POST("/create-order", rpHandler.CreateOrder)
|
||||
api.POST("/created-callback", rpHandler.CreatedCallback)
|
||||
api.GET("/detail", rpHandler.Detail)
|
||||
api.POST("/claim-sign", rpHandler.ClaimSign)
|
||||
api.POST("/claim-result", rpHandler.ClaimResult)
|
||||
}
|
||||
|
||||
// Admin APIs - should be protected with authentication in production
|
||||
admin := r.Group("/admin/redpacket")
|
||||
{
|
||||
admin.POST("/set-signer", adminHandler.SetSigner)
|
||||
admin.POST("/set-token", adminHandler.SetToken)
|
||||
admin.POST("/set-expiry", adminHandler.SetExpiry)
|
||||
admin.POST("/set-allow-all-tokens", adminHandler.SetAllowAllTokens)
|
||||
admin.POST("/set-native-token", adminHandler.SetNativeTokenEnabled)
|
||||
admin.POST("/parse-tx-events", adminHandler.ParseTxEvents)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user