mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-06-28 13:55:01 +08:00
更新红包代码 文档 client-integration-guide.md backend-api.md
This commit is contained in:
parent
9661adcb65
commit
392943654b
@ -3,6 +3,7 @@
|
||||
A Web3 Red Packet service supporting Ethereum and TRON, following the design documents:
|
||||
|
||||
- `backend-api.md` - API specifications
|
||||
- `client-integration-guide.md` - Frontend / gateway integration guide
|
||||
- `redpacket-web3-integration-design.md` - Architecture and flows
|
||||
- `red-packet-go-backend-eth-tron.md` - Blockchain integration details
|
||||
|
||||
@ -14,9 +15,30 @@ A Web3 Red Packet service supporting Ethereum and TRON, following the design doc
|
||||
- ✅ Claim signature issuance (`/api/redpacket/claim-sign`)
|
||||
- ✅ Claim result reporting
|
||||
- ✅ SQLite/MySQL support
|
||||
- ✅ Blockchain signature logic ready for ETH/TRON
|
||||
- ✅ EVM signature generation via `getSignMessage(...)`
|
||||
- ✅ Basic EVM event indexing for claim/refund synchronization
|
||||
- ✅ Idempotent claim/refund persistence by transaction hash
|
||||
- ✅ Admin configuration endpoints
|
||||
|
||||
## Current Status
|
||||
|
||||
This service is runnable and suitable for continued iteration, but it is not yet fully production-complete.
|
||||
|
||||
Working well now:
|
||||
|
||||
- EVM-side claim signing uses the real `authNonce` in the digest
|
||||
- Claim pre-checks cover packet existence, active status, expiry, and already-claimed cases
|
||||
- EVM ABI and event parsing are aligned with the current contract events
|
||||
- Claim and refund events can be persisted idempotently
|
||||
|
||||
Still incomplete:
|
||||
|
||||
- ETH admin endpoints are still mostly mock behavior
|
||||
- `PacketCreated` indexing is not yet fully wired for automatic order reconciliation
|
||||
- TRON `getSignMessage` flow is not complete
|
||||
- TRON event decoding is still a scaffold
|
||||
- Admin APIs still need authentication and audit controls
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
@ -64,7 +86,7 @@ curl -X POST http://localhost:8080/api/redpacket/create-order \
|
||||
│ ├── model/ # Database models (GORM)
|
||||
│ ├── repository/ # Data access layer
|
||||
│ ├── service/ # Business logic
|
||||
│ └── chain/ # Blockchain integration (to be expanded)
|
||||
│ └── chain/ # Blockchain integration and event indexing
|
||||
├── pkg/resp/ # Response helpers
|
||||
├── router/ # Route definitions
|
||||
├── main.go
|
||||
@ -72,27 +94,19 @@ curl -X POST http://localhost:8080/api/redpacket/create-order \
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Next Steps (from design docs)
|
||||
## Recommended Next Steps
|
||||
|
||||
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
|
||||
1. Implement real ETH admin transactions for signer/token/expiry configuration
|
||||
2. Finish `PacketCreated` indexing and automatic order reconciliation
|
||||
3. Complete TRON `getSignMessage` and reliable event decoding
|
||||
4. Add authentication, audit, and rate limiting for sensitive endpoints
|
||||
5. Extend end-to-end test coverage
|
||||
|
||||
See the three design documents for detailed specifications.
|
||||
|
||||
## API Documentation
|
||||
|
||||
See `backend-api.md` for complete API reference with examples.
|
||||
See:
|
||||
|
||||
- `backend-api.md` for complete API reference with request / response examples
|
||||
- `client-integration-guide.md` for frontend, wallet-binding, and claim-sign integration steps
|
||||
|
||||
@ -202,19 +202,59 @@ curl "http://127.0.0.1:8080/api/redpacket/detail?packet_id=10001"
|
||||
|
||||
先做业务鉴权,再发放 `claim(...)` 所需签名参数。
|
||||
|
||||
#### 鉴权说明
|
||||
|
||||
- 该接口不再信任请求体中的 `user_id`
|
||||
- 当前领取用户从 RPC / 网关注入的登录上下文中获取
|
||||
- 服务端要求请求上下文里存在 `opUserID`
|
||||
- 如果缺少登录上下文,接口会直接拒绝
|
||||
|
||||
#### 请求头
|
||||
|
||||
- `token`: 用户登录 token
|
||||
|
||||
> 约定:上游网关或鉴权中间件需要先解析 token,并把当前登录用户写入请求上下文中的 `opUserID`。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packet_id": "10001",
|
||||
"claimer": "0x3333333333333333333333333333333333333333",
|
||||
"user_id": "u2002",
|
||||
"random_seed": "0"
|
||||
}
|
||||
```
|
||||
|
||||
> `random_seed` 可选;传 `0` 或空时后端自动生成。
|
||||
|
||||
#### 字段说明
|
||||
|
||||
- `packet_id`: 红包链上 ID
|
||||
- `claimer`: 本次真正发起链上 `claim(...)` 的钱包地址
|
||||
- `random_seed`: 可选随机种子;空或 `0` 时后端自动生成
|
||||
|
||||
#### 服务端处理逻辑
|
||||
|
||||
1. 从请求上下文提取当前登录用户 ID
|
||||
2. 校验红包是否存在、是否过期、是否仍可领取
|
||||
3. 校验当前登录用户与 `claimer` 钱包地址的绑定关系
|
||||
4. 校验当前用户在该红包下是否已领取
|
||||
5. 校验当前钱包在该红包下是否已领取
|
||||
6. 按红包类型校验群资格 / 指定接收人资格
|
||||
7. 生成 `auth_nonce`、`deadline`、`random_seed`
|
||||
8. 调合约 `getSignMessage(packetId, claimer, authNonce, randomSeed, deadline)` 获取摘要
|
||||
9. 使用后端 `signer` 私钥对摘要裸签名
|
||||
10. 落库 `red_packet_claim_auth`
|
||||
11. 返回前端发链所需参数
|
||||
|
||||
#### 成功后前端下一步
|
||||
|
||||
前端拿到响应后,直接调用链上:
|
||||
|
||||
```text
|
||||
claim(packetId, authNonce, randomSeed, deadline, signature)
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
@ -241,6 +281,33 @@ curl "http://127.0.0.1:8080/api/redpacket/detail?packet_id=10001"
|
||||
}
|
||||
```
|
||||
|
||||
同一用户已领取:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"message": "user already claimed"
|
||||
}
|
||||
```
|
||||
|
||||
钱包未绑定:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"message": "wallet is not bound to user"
|
||||
}
|
||||
```
|
||||
|
||||
缺少登录上下文:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"message": "op user id missing in context"
|
||||
}
|
||||
```
|
||||
|
||||
签名服务异常:
|
||||
|
||||
```json
|
||||
@ -258,17 +325,44 @@ curl "http://127.0.0.1:8080/api/redpacket/detail?packet_id=10001"
|
||||
|
||||
前端在领取交易提交后可调用该接口预写记录。最终状态仍以链监听(indexer)为准。
|
||||
|
||||
#### 鉴权说明
|
||||
|
||||
- 该接口不再接收可信 `user_id`
|
||||
- 当前用户从 RPC / 网关注入的登录上下文中获取
|
||||
- 服务端要求请求上下文里存在 `opUserID`
|
||||
|
||||
#### 请求头
|
||||
|
||||
- `token`: 用户登录 token
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packet_id": "10001",
|
||||
"claimer_wallet": "0x3333333333333333333333333333333333333333",
|
||||
"tx_hash": "0xdef456...",
|
||||
"auth_nonce": "328840239847239847"
|
||||
"claimer": "0x3333333333333333333333333333333333333333",
|
||||
"tx_hash": "0xdef456..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
- `packet_id`: 红包链上 ID
|
||||
- `claimer`: 发起链上领取的钱包地址
|
||||
- `tx_hash`: 领取交易哈希
|
||||
|
||||
#### 服务端处理逻辑
|
||||
|
||||
1. 从请求上下文提取当前登录用户 ID
|
||||
2. 先落一条 `PENDING` 领取记录
|
||||
3. 如果当前节点能立即解析该交易 receipt,则补全:
|
||||
- `auth_nonce`
|
||||
- `claimed_amount`
|
||||
- `block_number`
|
||||
- `status=CONFIRMED`
|
||||
4. 如果当前节点暂时拿不到 receipt,则保持 `PENDING`
|
||||
5. 最终仍以链监听器写入结果为准
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
@ -285,8 +379,130 @@ curl "http://127.0.0.1:8080/api/redpacket/detail?packet_id=10001"
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "packet_id and tx_hash are required"
|
||||
"code": 403,
|
||||
"message": "op user id missing in context"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6) 钱包绑定挑战
|
||||
|
||||
### POST `/api/redpacket/wallet-bind/challenge`
|
||||
|
||||
生成钱包绑定挑战消息,前端拿到消息后调用钱包签名。
|
||||
|
||||
#### 鉴权说明
|
||||
|
||||
- 该接口不再信任请求体中的 `user_id`
|
||||
- 当前用户从 RPC / 网关注入的登录上下文中获取
|
||||
|
||||
#### 请求头
|
||||
|
||||
- `token`: 用户登录 token
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"chain_type": "EVM",
|
||||
"chain_id": 1,
|
||||
"wallet_address": "0x3333333333333333333333333333333333333333",
|
||||
"domain": "redpacket.example.com",
|
||||
"uri": "https://redpacket.example.com/wallet-bind"
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"challenge_id": "1f7d9b0d-7b43-4d84-bb11-65f2ecf7e321",
|
||||
"user_id": "u2002",
|
||||
"chain_type": "EVM",
|
||||
"chain_id": 1,
|
||||
"wallet": "0x3333333333333333333333333333333333333333",
|
||||
"protocol": "siwe-eip4361",
|
||||
"sign_method": "personal_sign",
|
||||
"nonce": "7b7d8d48-9db6-4e95-9daa-40e9517a2a85",
|
||||
"message": "redpacket.example.com wants you to sign in with your Ethereum account:\n0x3333333333333333333333333333333333333333\n\nBind wallet 0x3333333333333333333333333333333333333333 to user u2002.\nURI: https://redpacket.example.com/wallet-bind\nVersion: 1\nChain ID: 1\nNonce: 7b7d8d48-9db6-4e95-9daa-40e9517a2a85\nIssued At: 2026-04-30T03:00:00Z\nExpiration Time: 2026-04-30T03:10:00Z\nRequest ID: 1f7d9b0d-7b43-4d84-bb11-65f2ecf7e321",
|
||||
"issued_at": "2026-04-30T03:00:00Z",
|
||||
"expires_at": "2026-04-30T03:10:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 前端下一步
|
||||
|
||||
前端收到响应后:
|
||||
|
||||
1. 使用 `sign_method` 指定的钱包方法对 `message` 进行签名
|
||||
2. 把 `challenge_id + signature` 提交给 `/api/redpacket/wallet-bind/confirm`
|
||||
|
||||
---
|
||||
|
||||
## 7) 钱包绑定确认
|
||||
|
||||
### POST `/api/redpacket/wallet-bind/confirm`
|
||||
|
||||
提交钱包签名,服务端验签成功后建立钱包绑定关系。
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"challenge_id": "1f7d9b0d-7b43-4d84-bb11-65f2ecf7e321",
|
||||
"signature": "0x8f..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"user_id": "u2002",
|
||||
"chain_type": "EVM",
|
||||
"chain_id": 1,
|
||||
"wallet_address": "0x3333333333333333333333333333333333333333",
|
||||
"status": "ACTIVE",
|
||||
"verified_at": "2026-04-30T03:01:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8) 查询钱包绑定
|
||||
|
||||
### GET `/api/redpacket/wallet-bind/detail?chain_type={chainType}&wallet_address={walletAddress}`
|
||||
|
||||
查询当前登录用户与指定钱包地址的绑定详情。
|
||||
|
||||
#### 鉴权说明
|
||||
|
||||
- `user_id` 从登录上下文中获取,不需要也不应该由前端传入
|
||||
|
||||
#### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"user_id": "u2002",
|
||||
"chain_type": "EVM",
|
||||
"chain_id": 1,
|
||||
"wallet_address": "0x3333333333333333333333333333333333333333",
|
||||
"status": "ACTIVE",
|
||||
"challenge_id": "1f7d9b0d-7b43-4d84-bb11-65f2ecf7e321",
|
||||
"verified_at": "2026-04-30T03:01:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -503,4 +719,3 @@ TRON 未配置:
|
||||
6. 钱包调用合约 `claim(...)`
|
||||
7. 可选:`POST /api/redpacket/claim-result`
|
||||
8. 详情页查询:`GET /api/redpacket/detail?packet_id=...`
|
||||
|
||||
|
||||
271
cmd/openim-rpc/openim-rpc-redpacket/client-integration-guide.md
Normal file
271
cmd/openim-rpc/openim-rpc-redpacket/client-integration-guide.md
Normal file
@ -0,0 +1,271 @@
|
||||
# RedPacket 前端对接文档
|
||||
|
||||
本文档面向前端 / 网关 / App 对接方,说明红包领取和钱包绑定的真实接入方式,重点覆盖:
|
||||
|
||||
- 如何把当前登录用户传递给红包服务
|
||||
- 如何绑定钱包
|
||||
- 如何申请领取签名
|
||||
- 前端何时发链、何时回写后端
|
||||
|
||||
## 1. 总体原则
|
||||
|
||||
红包服务已经切换为 RPC 上下文取当前用户 ID:
|
||||
|
||||
- 前端不再把 `user_id` 当作可信业务参数传给红包服务
|
||||
- 红包服务从请求上下文里的 `opUserID` 获取当前登录用户
|
||||
- 上下文通常由网关或鉴权中间件根据 `token` 解析后注入
|
||||
|
||||
这意味着对接时必须满足一个前提:
|
||||
|
||||
- 请求进入红包服务前,网关已经完成 token 解析
|
||||
- 并且把当前登录用户写入上下文中的 `opUserID`
|
||||
|
||||
如果没有这一层,红包服务会返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"message": "op user id missing in context"
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 钱包绑定流程
|
||||
|
||||
### 2.1 流程图
|
||||
|
||||
```text
|
||||
前端 -> 红包服务: POST /api/redpacket/wallet-bind/challenge
|
||||
红包服务 -> 前端: challenge_id + message + sign_method
|
||||
前端 -> 钱包: 对 message 签名
|
||||
前端 -> 红包服务: POST /api/redpacket/wallet-bind/confirm
|
||||
红包服务 -> 前端: 绑定成功
|
||||
```
|
||||
|
||||
### 2.2 发起挑战
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
POST /api/redpacket/wallet-bind/challenge
|
||||
token: <user token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"chain_type": "EVM",
|
||||
"chain_id": 1,
|
||||
"wallet_address": "0x3333333333333333333333333333333333333333",
|
||||
"domain": "redpacket.example.com",
|
||||
"uri": "https://redpacket.example.com/wallet-bind"
|
||||
}
|
||||
```
|
||||
|
||||
返回里最关键的是:
|
||||
|
||||
- `challenge_id`
|
||||
- `message`
|
||||
- `sign_method`
|
||||
|
||||
前端要做的是:
|
||||
|
||||
- 按 `sign_method` 调钱包签名
|
||||
- 当前 EVM 实现使用的是 `personal_sign`
|
||||
|
||||
### 2.3 确认绑定
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
POST /api/redpacket/wallet-bind/confirm
|
||||
token: <user token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"challenge_id": "1f7d9b0d-7b43-4d84-bb11-65f2ecf7e321",
|
||||
"signature": "0x8f..."
|
||||
}
|
||||
```
|
||||
|
||||
成功后代表:
|
||||
|
||||
- 当前登录用户
|
||||
- 当前链类型
|
||||
- 当前钱包地址
|
||||
|
||||
已经在后端建立了有效绑定关系。
|
||||
|
||||
## 3. 领取签名流程
|
||||
|
||||
### 3.1 流程图
|
||||
|
||||
```text
|
||||
前端 -> 红包服务: POST /api/redpacket/claim-sign
|
||||
红包服务 -> 红包服务: 校验当前用户、钱包绑定、领取资格
|
||||
红包服务 -> 合约: getSignMessage(packetId, claimer, authNonce, randomSeed, deadline)
|
||||
红包服务 -> 前端: auth_nonce + random_seed + deadline + signature
|
||||
前端 -> 钱包/链上: claim(packetId, authNonce, randomSeed, deadline, signature)
|
||||
前端 -> 红包服务: POST /api/redpacket/claim-result (可选)
|
||||
链监听器 -> 红包服务: 最终确认领取结果
|
||||
```
|
||||
|
||||
### 3.2 申请领取签名
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
POST /api/redpacket/claim-sign
|
||||
token: <user token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"packet_id": "10001",
|
||||
"claimer": "0x3333333333333333333333333333333333333333",
|
||||
"random_seed": "0"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `claimer` 必须是这次真正发链的地址
|
||||
- `random_seed` 可省略或传 `0`
|
||||
- 不需要传 `user_id`
|
||||
|
||||
后端会自动完成这些校验:
|
||||
|
||||
1. 当前登录用户存在
|
||||
2. 红包存在且仍可领取
|
||||
3. 当前登录用户与 `claimer` 已绑定
|
||||
4. 当前用户在该红包下未领取过
|
||||
5. 当前钱包在该红包下未领取过
|
||||
6. 群红包 / 转账红包的附加业务限制通过
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"auth_nonce": "328840239847239847",
|
||||
"deadline": 1777012345,
|
||||
"signature": "0x7b1e...a2",
|
||||
"random_seed": "8888812345"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 前端拿到响应后要做什么
|
||||
|
||||
前端必须原样把这些参数传给链上:
|
||||
|
||||
```text
|
||||
claim(packetId, authNonce, randomSeed, deadline, signature)
|
||||
```
|
||||
|
||||
对应关系:
|
||||
|
||||
- `packetId` -> 前端当前红包 ID
|
||||
- `authNonce` -> 响应里的 `auth_nonce`
|
||||
- `randomSeed` -> 响应里的 `random_seed`
|
||||
- `deadline` -> 响应里的 `deadline`
|
||||
- `signature` -> 响应里的 `signature`
|
||||
|
||||
注意:
|
||||
|
||||
- 不要自己改 `auth_nonce`
|
||||
- 不要重新算摘要
|
||||
- 不要对摘要再次做 `signMessage`
|
||||
- 后端返回的 `signature` 已经是最终可上链签名
|
||||
|
||||
## 4. 领取结果回写
|
||||
|
||||
`claim-result` 是可选的,主要作用是让业务侧尽快看到一条 `PENDING` 领取记录。
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
POST /api/redpacket/claim-result
|
||||
token: <user token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"packet_id": "10001",
|
||||
"claimer": "0x3333333333333333333333333333333333333333",
|
||||
"tx_hash": "0xdef456..."
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 不需要传 `user_id`
|
||||
- 当前登录用户仍然从上下文中取
|
||||
- 如果后端当前能立刻解析 receipt,会把记录补成 `CONFIRMED`
|
||||
- 如果不能,会先记成 `PENDING`
|
||||
- 最终仍以链监听器为准
|
||||
|
||||
## 5. 前端推荐调用顺序
|
||||
|
||||
### 5.1 首次使用钱包领取
|
||||
|
||||
1. 用户登录业务系统
|
||||
2. 前端请求 `/wallet-bind/challenge`
|
||||
3. 钱包对 `message` 签名
|
||||
4. 前端请求 `/wallet-bind/confirm`
|
||||
5. 绑定成功后再进入领取流程
|
||||
|
||||
### 5.2 正常领取
|
||||
|
||||
1. 前端拿到红包 `packet_id`
|
||||
2. 用户连接钱包,得到本次 `claimer` 地址
|
||||
3. 前端请求 `/claim-sign`
|
||||
4. 拿到 `auth_nonce + random_seed + deadline + signature`
|
||||
5. 前端调用链上 `claim(...)`
|
||||
6. 前端可选请求 `/claim-result`
|
||||
7. 页面轮询详情页或等待业务侧状态同步
|
||||
|
||||
## 6. 常见错误和排查
|
||||
|
||||
### 6.1 `op user id missing in context`
|
||||
|
||||
原因:
|
||||
|
||||
- 网关没有解析 token
|
||||
- 网关没有把 `opUserID` 注入上下文
|
||||
- 直接绕过网关调用了红包服务
|
||||
|
||||
### 6.2 `wallet is not bound to user`
|
||||
|
||||
原因:
|
||||
|
||||
- 当前钱包还没绑定
|
||||
- 当前钱包绑定的是别的业务用户
|
||||
- 链类型不一致
|
||||
|
||||
### 6.3 `already claimed`
|
||||
|
||||
原因:
|
||||
|
||||
- 同一个钱包地址已经领过该红包
|
||||
|
||||
### 6.4 `user already claimed`
|
||||
|
||||
原因:
|
||||
|
||||
- 同一个业务用户已经领取过该红包
|
||||
- 即使换钱包地址,也会被后端拦截
|
||||
|
||||
## 7. 后端接口与代码位置
|
||||
|
||||
- 接口契约文档:
|
||||
[backend-api.md](/Users/panda/aiCode/red_packet/open-im-server-origin/cmd/openim-rpc/openim-rpc-redpacket/backend-api.md)
|
||||
- 领取签名核心逻辑:
|
||||
[redpacket.go](/Users/panda/aiCode/red_packet/open-im-server-origin/cmd/openim-rpc/openim-rpc-redpacket/internal/service/redpacket.go)
|
||||
- 用户上下文提取:
|
||||
[user.go](/Users/panda/aiCode/red_packet/open-im-server-origin/cmd/openim-rpc/openim-rpc-redpacket/internal/authctx/user.go)
|
||||
@ -17,7 +17,7 @@ tron:
|
||||
contract_base58: ""
|
||||
owner_base58: ""
|
||||
private_key_hex: ""
|
||||
fee_limit: 100000000
|
||||
fee_limit: 10000000000
|
||||
|
||||
indexer:
|
||||
poll_interval: 5
|
||||
|
||||
49
cmd/openim-rpc/openim-rpc-redpacket/internal/authctx/user.go
Normal file
49
cmd/openim-rpc/openim-rpc-redpacket/internal/authctx/user.go
Normal file
@ -0,0 +1,49 @@
|
||||
package authctx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const opUserIDKey = "opUserID"
|
||||
|
||||
type userIDContextKey struct{}
|
||||
|
||||
func WithCurrentUserID(ctx context.Context, userID string) context.Context {
|
||||
return context.WithValue(ctx, userIDContextKey{}, strings.TrimSpace(userID))
|
||||
}
|
||||
|
||||
func CurrentUserID(ctx context.Context) (string, error) {
|
||||
if ctx == nil {
|
||||
return "", fmt.Errorf("request context is nil")
|
||||
}
|
||||
if userID, ok := ctx.Value(userIDContextKey{}).(string); ok && strings.TrimSpace(userID) != "" {
|
||||
return strings.TrimSpace(userID), nil
|
||||
}
|
||||
if userID, ok := ctx.Value(opUserIDKey).(string); ok && strings.TrimSpace(userID) != "" {
|
||||
return strings.TrimSpace(userID), nil
|
||||
}
|
||||
return "", fmt.Errorf("op user id missing in context")
|
||||
}
|
||||
|
||||
func BindCurrentUserID(c *gin.Context) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("gin context is nil")
|
||||
}
|
||||
userID := strings.TrimSpace(c.GetString(opUserIDKey))
|
||||
if userID == "" {
|
||||
if value := c.Request.Context().Value(opUserIDKey); value != nil {
|
||||
if fromCtx, ok := value.(string); ok {
|
||||
userID = strings.TrimSpace(fromCtx)
|
||||
}
|
||||
}
|
||||
}
|
||||
if userID == "" {
|
||||
return fmt.Errorf("op user id missing in context")
|
||||
}
|
||||
c.Request = c.Request.WithContext(WithCurrentUserID(c.Request.Context(), userID))
|
||||
return nil
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "packetId", "type": "uint256" },
|
||||
{ "indexed": true, "name": "creator", "type": "address" },
|
||||
{ "indexed": false, "name": "packetType", "type": "uint8" },
|
||||
{ "indexed": true, "name": "packetType", "type": "uint8" },
|
||||
{ "indexed": false, "name": "token", "type": "address" },
|
||||
{ "indexed": false, "name": "totalAmount", "type": "uint256" },
|
||||
{ "indexed": false, "name": "totalShares", "type": "uint256" },
|
||||
@ -19,9 +19,9 @@
|
||||
{ "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" }
|
||||
{ "indexed": false, "name": "remainingAmount", "type": "uint256" },
|
||||
{ "indexed": false, "name": "remainingShares", "type": "uint256" },
|
||||
{ "indexed": false, "name": "authNonce", "type": "uint256" }
|
||||
],
|
||||
"name": "PacketClaimed",
|
||||
"type": "event"
|
||||
@ -30,6 +30,7 @@
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "packetId", "type": "uint256" },
|
||||
{ "indexed": true, "name": "operator", "type": "address" },
|
||||
{ "indexed": true, "name": "refundTo", "type": "address" },
|
||||
{ "indexed": false, "name": "amount", "type": "uint256" }
|
||||
],
|
||||
@ -62,4 +63,4 @@
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@ -3,6 +3,7 @@ package chain
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
@ -14,14 +15,17 @@ import (
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
//go:embed abi/RedPacket.json
|
||||
var embeddedABI []byte
|
||||
|
||||
// ChainClient handles blockchain interactions for RedPacket
|
||||
type ChainClient struct {
|
||||
client *ethclient.Client
|
||||
contractABI abi.ABI
|
||||
contractAddr common.Address
|
||||
signerKey *ecdsa.PrivateKey
|
||||
client *ethclient.Client
|
||||
contractABI abi.ABI
|
||||
contractAddr common.Address
|
||||
signerKey *ecdsa.PrivateKey
|
||||
configAdminKey *ecdsa.PrivateKey
|
||||
chainID *big.Int
|
||||
chainID *big.Int
|
||||
}
|
||||
|
||||
// NewClient creates a new ChainClient
|
||||
@ -122,6 +126,17 @@ func (c *ChainClient) ParseTransactionReceipt(ctx context.Context, txHash common
|
||||
return ParseEventsFromLogs(receipt.Logs, c.contractABI)
|
||||
}
|
||||
|
||||
func (c *ChainClient) ContractAddress() common.Address {
|
||||
return c.contractAddr
|
||||
}
|
||||
|
||||
func (c *ChainClient) ChainID() *big.Int {
|
||||
if c.chainID == nil {
|
||||
return nil
|
||||
}
|
||||
return new(big.Int).Set(c.chainID)
|
||||
}
|
||||
|
||||
// Close closes the client connection
|
||||
func (c *ChainClient) Close() {
|
||||
if c.client != nil {
|
||||
@ -131,13 +146,8 @@ func (c *ChainClient) 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
|
||||
if len(embeddedABI) == 0 {
|
||||
return nil, fmt.Errorf("embedded ABI is empty")
|
||||
}
|
||||
return embeddedABI, nil
|
||||
}
|
||||
|
||||
@ -17,10 +17,10 @@ import (
|
||||
|
||||
// Indexer listens to blockchain events and updates database
|
||||
type Indexer struct {
|
||||
client *ChainClient
|
||||
repo repository.Repository
|
||||
client *ChainClient
|
||||
repo repository.Repository
|
||||
pollInterval time.Duration
|
||||
lastBlock uint64
|
||||
lastBlock uint64
|
||||
contractAddr common.Address
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ func (i *Indexer) processEvent(ctx context.Context, event *ParsedEvent, logs []*
|
||||
|
||||
func (i *Indexer) handlePacketCreated(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
creator := GetClaimerFromEvent(event) // creator is indexed as second topic
|
||||
creator := GetAddressFromEvent(event, "creator")
|
||||
|
||||
log.Printf("📦 PacketCreated: packetId=%s, creator=%s", packetID.String(), creator.Hex())
|
||||
|
||||
@ -135,29 +135,53 @@ func (i *Indexer) handlePacketCreated(ctx context.Context, event *ParsedEvent) e
|
||||
|
||||
func (i *Indexer) handlePacketClaimed(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
claimer := GetClaimerFromEvent(event)
|
||||
claimer := GetAddressFromEvent(event, "claimer")
|
||||
amount := GetAmountFromEvent(event)
|
||||
authNonce := GetUintFromEvent(event, "authNonce")
|
||||
|
||||
log.Printf("🎁 PacketClaimed: packetId=%s, claimer=%s, amount=%s",
|
||||
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(),
|
||||
AuthNonce: authNonce.String(),
|
||||
ClaimTxHash: event.TxHash.Hex(),
|
||||
ClaimedAmount: amount.String(),
|
||||
BlockNumber: event.BlockNumber,
|
||||
Status: "CONFIRMED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
return i.repo.CreateClaim(ctx, claim)
|
||||
if err := i.repo.SaveClaim(ctx, claim); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := i.repo.MarkClaimAuthUsed(ctx, authNonce.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return i.repo.UpdateRedPacketClaimProgress(ctx, packetID.String(), amount.String(), "")
|
||||
}
|
||||
|
||||
func (i *Indexer) handlePacketRefunded(ctx context.Context, event *ParsedEvent) error {
|
||||
packetID := GetPacketIDFromEvent(event)
|
||||
refundTo := GetClaimerFromEvent(event) // refundTo is indexed
|
||||
operator := GetAddressFromEvent(event, "operator")
|
||||
refundTo := GetAddressFromEvent(event, "refundTo")
|
||||
amount := GetAmountFromEvent(event)
|
||||
|
||||
log.Printf("♻️ PacketRefunded: packetId=%s, refundTo=%s", packetID.String(), refundTo.Hex())
|
||||
log.Printf("♻️ PacketRefunded: packetId=%s, operator=%s, refundTo=%s, amount=%s",
|
||||
packetID.String(), operator.Hex(), refundTo.Hex(), amount.String())
|
||||
|
||||
// TODO: Update packet status to REFUNDED
|
||||
return nil
|
||||
if err := i.repo.SaveRefund(ctx, &model.RedPacketRefund{
|
||||
PacketID: packetID.String(),
|
||||
RefundTo: refundTo.Hex(),
|
||||
TxHash: event.TxHash.Hex(),
|
||||
Amount: amount.String(),
|
||||
CreatedAt: time.Now(),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return i.repo.UpdateRedPacketStatus(ctx, packetID.String(), "REFUNDED")
|
||||
}
|
||||
|
||||
@ -11,8 +11,10 @@ import (
|
||||
|
||||
// ParsedEvent represents a parsed blockchain event
|
||||
type ParsedEvent struct {
|
||||
Name string
|
||||
Data map[string]interface{}
|
||||
Name string
|
||||
Data map[string]interface{}
|
||||
TxHash common.Hash
|
||||
BlockNumber uint64
|
||||
}
|
||||
|
||||
// ParseEventsFromLogs parses logs using the contract ABI
|
||||
@ -75,8 +77,10 @@ func parseEvent(log *types.Log, contractABI abi.ABI) (*ParsedEvent, error) {
|
||||
}
|
||||
|
||||
return &ParsedEvent{
|
||||
Name: name,
|
||||
Data: data,
|
||||
Name: name,
|
||||
Data: data,
|
||||
TxHash: log.TxHash,
|
||||
BlockNumber: log.BlockNumber,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -94,21 +98,28 @@ func GetPacketIDFromEvent(event *ParsedEvent) *big.Int {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
func GetAddressFromEvent(event *ParsedEvent, key string) common.Address {
|
||||
value, ok := event.Data[key]
|
||||
if !ok {
|
||||
return common.Address{}
|
||||
}
|
||||
return common.Address{}
|
||||
addr, _ := value.(common.Address)
|
||||
return addr
|
||||
}
|
||||
|
||||
// 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 GetUintFromEvent(event, "amount")
|
||||
}
|
||||
|
||||
// GetUintFromEvent extracts a uint field from event data.
|
||||
func GetUintFromEvent(event *ParsedEvent, key string) *big.Int {
|
||||
value, ok := event.Data[key]
|
||||
if !ok {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
if b, ok := value.(*big.Int); ok {
|
||||
return b
|
||||
}
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
func TestParseEventsFromLogs_ParsesRefundEvent(t *testing.T) {
|
||||
abiJSON, err := ExtractABIFromEmbeddedArtifact()
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractABIFromEmbeddedArtifact() error = %v", err)
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(string(abiJSON)))
|
||||
if err != nil {
|
||||
t.Fatalf("abi.JSON() error = %v", err)
|
||||
}
|
||||
|
||||
eventDef := parsedABI.Events["PacketRefunded"]
|
||||
packetID := big.NewInt(101)
|
||||
operator := common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||
refundTo := common.HexToAddress("0x2222222222222222222222222222222222222222")
|
||||
amount := big.NewInt(8888)
|
||||
|
||||
data, err := eventDef.Inputs.NonIndexed().Pack(amount)
|
||||
if err != nil {
|
||||
t.Fatalf("Pack() error = %v", err)
|
||||
}
|
||||
|
||||
log := &types.Log{
|
||||
Address: common.HexToAddress("0x3333333333333333333333333333333333333333"),
|
||||
Topics: []common.Hash{
|
||||
eventDef.ID,
|
||||
common.BigToHash(packetID),
|
||||
common.BytesToHash(common.LeftPadBytes(operator.Bytes(), 32)),
|
||||
common.BytesToHash(common.LeftPadBytes(refundTo.Bytes(), 32)),
|
||||
},
|
||||
Data: data,
|
||||
BlockNumber: 77,
|
||||
TxHash: common.HexToHash("0xabc"),
|
||||
}
|
||||
|
||||
events, err := ParseEventsFromLogs([]*types.Log{log}, parsedABI)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseEventsFromLogs() error = %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
|
||||
event := events[0]
|
||||
if event.Name != "PacketRefunded" {
|
||||
t.Fatalf("unexpected event name: %s", event.Name)
|
||||
}
|
||||
if got := GetPacketIDFromEvent(event).String(); got != "101" {
|
||||
t.Fatalf("packet id mismatch: got %s", got)
|
||||
}
|
||||
if got := GetAddressFromEvent(event, "operator").Hex(); got != operator.Hex() {
|
||||
t.Fatalf("operator mismatch: got %s want %s", got, operator.Hex())
|
||||
}
|
||||
if got := GetAddressFromEvent(event, "refundTo").Hex(); got != refundTo.Hex() {
|
||||
t.Fatalf("refundTo mismatch: got %s want %s", got, refundTo.Hex())
|
||||
}
|
||||
if got := GetAmountFromEvent(event).String(); got != "8888" {
|
||||
t.Fatalf("amount mismatch: got %s", got)
|
||||
}
|
||||
if event.BlockNumber != 77 {
|
||||
t.Fatalf("block number mismatch: got %d", event.BlockNumber)
|
||||
}
|
||||
if event.TxHash != common.HexToHash("0xabc") {
|
||||
t.Fatalf("tx hash mismatch: got %s", event.TxHash.Hex())
|
||||
}
|
||||
}
|
||||
@ -13,17 +13,18 @@ import (
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// TronClient handles TRON blockchain interactions using HTTP JSON-RPC
|
||||
type TronClient struct {
|
||||
fullNodeURL string
|
||||
fullNodeURL string
|
||||
contractBase58 string
|
||||
ownerBase58 string
|
||||
privateKeyHex string
|
||||
feeLimit int64
|
||||
abiJSON string
|
||||
parsedABI abi.ABI
|
||||
ownerBase58 string
|
||||
privateKeyHex string
|
||||
feeLimit int64
|
||||
abiJSON string
|
||||
parsedABI abi.ABI
|
||||
}
|
||||
|
||||
// NewTronClient creates a new TRON client
|
||||
@ -48,6 +49,24 @@ func NewTronClient(fullNodeURL, contractBase58, ownerBase58, privateKeyHex strin
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TronClient) ContractAddress() string {
|
||||
return t.contractBase58
|
||||
}
|
||||
|
||||
func (t *TronClient) ParseTransactionReceipt(ctx context.Context, txID string) ([]*ParsedEvent, error) {
|
||||
info, err := t.getTransactionInfo(ctx, txID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs, err := tronLogsToEVMLogs(info, txID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ParseEventsFromLogs(logs, t.parsedABI)
|
||||
}
|
||||
|
||||
// SendAdminTransaction sends an admin transaction on TRON (setSigner, setToken, etc.)
|
||||
func (t *TronClient) SendAdminTransaction(ctx context.Context, methodName string, args ...interface{}) (string, error) {
|
||||
if t.privateKeyHex == "" || t.ownerBase58 == "" {
|
||||
@ -86,6 +105,16 @@ func (t *TronClient) GetSignMessageForTron(ctx context.Context, packetID *big.In
|
||||
return "", fmt.Errorf("TRON getSignMessage not fully implemented yet - use ETH path for signing")
|
||||
}
|
||||
|
||||
type tronTxInfoResp struct {
|
||||
ID string `json:"id"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
Log []struct {
|
||||
Address string `json:"address"`
|
||||
Topics []string `json:"topics"`
|
||||
Data string `json:"data"`
|
||||
} `json:"log"`
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func getParamTypes(args []interface{}) string {
|
||||
@ -165,6 +194,62 @@ func SendTronAdminTx(
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func (t *TronClient) getTransactionInfo(ctx context.Context, txID string) (*tronTxInfoResp, error) {
|
||||
var info tronTxInfoResp
|
||||
if err := postJSON(ctx, t.fullNodeURL+"/wallet/gettransactioninfobyid", map[string]interface{}{
|
||||
"value": txID,
|
||||
}, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func tronLogsToEVMLogs(info *tronTxInfoResp, txID string) ([]*types.Log, error) {
|
||||
if info == nil {
|
||||
return nil, fmt.Errorf("tron tx info is nil")
|
||||
}
|
||||
|
||||
txHash := common.HexToHash(addHexPrefix(txID))
|
||||
logs := make([]*types.Log, 0, len(info.Log))
|
||||
for _, entry := range info.Log {
|
||||
topics := make([]common.Hash, 0, len(entry.Topics))
|
||||
for _, topic := range entry.Topics {
|
||||
topics = append(topics, common.HexToHash(addHexPrefix(topic)))
|
||||
}
|
||||
|
||||
data, err := hex.DecodeString(strings.TrimPrefix(entry.Data, "0x"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode tron log data failed: %w", err)
|
||||
}
|
||||
|
||||
logs = append(logs, &types.Log{
|
||||
Address: tronLogAddressToCommonAddress(entry.Address),
|
||||
Topics: topics,
|
||||
Data: data,
|
||||
BlockNumber: info.BlockNumber,
|
||||
TxHash: txHash,
|
||||
})
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func tronLogAddressToCommonAddress(raw string) common.Address {
|
||||
raw = strings.TrimPrefix(raw, "0x")
|
||||
raw = strings.TrimPrefix(raw, "41")
|
||||
if len(raw) > 40 {
|
||||
raw = raw[len(raw)-40:]
|
||||
}
|
||||
return common.HexToAddress(addHexPrefix(raw))
|
||||
}
|
||||
|
||||
func addHexPrefix(value string) string {
|
||||
if strings.HasPrefix(value, "0x") || strings.HasPrefix(value, "0X") {
|
||||
return value
|
||||
}
|
||||
return "0x" + value
|
||||
}
|
||||
|
||||
func encodeTronParams(abiJSON, method string, args ...interface{}) (string, error) {
|
||||
parsed, err := abi.JSON(strings.NewReader(abiJSON))
|
||||
if err != nil {
|
||||
|
||||
@ -15,9 +15,9 @@ type TronIndexer struct {
|
||||
client *TronClient
|
||||
repo repository.Repository
|
||||
pollInterval time.Duration
|
||||
lastBlockNum int64 // TRON uses block numbers
|
||||
lastBlockNum int64 // TRON uses block numbers
|
||||
contractAddress string
|
||||
processedTxs map[string]bool // Simple dedup for this session
|
||||
processedTxs map[string]bool // Simple dedup for this session
|
||||
}
|
||||
|
||||
// NewTronIndexer creates a new TRON event indexer
|
||||
@ -223,7 +223,7 @@ func (t *TronIndexer) handleTronPacketClaimed(ctx context.Context, logData map[s
|
||||
Status: "CONFIRMED",
|
||||
}
|
||||
|
||||
if err := t.repo.CreateClaim(ctx, claim); err != nil {
|
||||
if err := t.repo.SaveClaim(ctx, claim); err != nil {
|
||||
log.Printf("Failed to save TRON claim: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
func TestTronLogsToEVMLogsAndParsePacketCreated(t *testing.T) {
|
||||
abiJSON, err := ExtractABIFromEmbeddedArtifact()
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractABIFromEmbeddedArtifact() error = %v", err)
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(string(abiJSON)))
|
||||
if err != nil {
|
||||
t.Fatalf("abi.JSON() error = %v", err)
|
||||
}
|
||||
|
||||
eventDef := parsedABI.Events["PacketCreated"]
|
||||
packetID := big.NewInt(12)
|
||||
creator := common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||
packetType := big.NewInt(1)
|
||||
token := common.HexToAddress("0x2222222222222222222222222222222222222222")
|
||||
totalAmount := big.NewInt(1000)
|
||||
totalShares := big.NewInt(10)
|
||||
expiryAt := big.NewInt(1234567890)
|
||||
|
||||
data, err := eventDef.Inputs.NonIndexed().Pack(token, totalAmount, totalShares, expiryAt)
|
||||
if err != nil {
|
||||
t.Fatalf("Pack() error = %v", err)
|
||||
}
|
||||
|
||||
info := &tronTxInfoResp{
|
||||
ID: "abc123",
|
||||
BlockNumber: 88,
|
||||
Log: []struct {
|
||||
Address string `json:"address"`
|
||||
Topics []string `json:"topics"`
|
||||
Data string `json:"data"`
|
||||
}{
|
||||
{
|
||||
Address: "41aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
Topics: []string{
|
||||
strings.TrimPrefix(eventDef.ID.Hex(), "0x"),
|
||||
strings.TrimPrefix(common.BigToHash(packetID).Hex(), "0x"),
|
||||
strings.TrimPrefix(common.BytesToHash(common.LeftPadBytes(creator.Bytes(), 32)).Hex(), "0x"),
|
||||
strings.TrimPrefix(common.BigToHash(packetType).Hex(), "0x"),
|
||||
},
|
||||
Data: common.Bytes2Hex(data),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
logs, err := tronLogsToEVMLogs(info, info.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("tronLogsToEVMLogs() error = %v", err)
|
||||
}
|
||||
|
||||
events, err := ParseEventsFromLogs(logs, parsedABI)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseEventsFromLogs() error = %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
|
||||
event := events[0]
|
||||
if event.Name != "PacketCreated" {
|
||||
t.Fatalf("unexpected event name: %s", event.Name)
|
||||
}
|
||||
if got := GetPacketIDFromEvent(event).String(); got != "12" {
|
||||
t.Fatalf("packet id mismatch: got %s", got)
|
||||
}
|
||||
if got := GetAddressFromEvent(event, "creator").Hex(); got != creator.Hex() {
|
||||
t.Fatalf("creator mismatch: got %s want %s", got, creator.Hex())
|
||||
}
|
||||
if got := GetUintFromEvent(event, "packetType").String(); got != "1" {
|
||||
t.Fatalf("packetType mismatch: got %s", got)
|
||||
}
|
||||
if got := GetAddressFromEvent(event, "token").Hex(); got != token.Hex() {
|
||||
t.Fatalf("token mismatch: got %s want %s", got, token.Hex())
|
||||
}
|
||||
if event.BlockNumber != 88 {
|
||||
t.Fatalf("block number mismatch: got %d", event.BlockNumber)
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"redpacket/internal/authctx"
|
||||
"redpacket/internal/service"
|
||||
"redpacket/pkg/resp"
|
||||
|
||||
@ -18,6 +19,11 @@ func NewRedPacketHandler(rpSvc *service.RedPacketService) *RedPacketHandler {
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) CreateOrder(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req service.CreateOrderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
@ -65,10 +71,14 @@ func (h *RedPacketHandler) Detail(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) ClaimSign(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PacketID string `json:"packet_id" binding:"required"`
|
||||
Claimer string `json:"claimer" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
RandomSeed string `json:"random_seed"`
|
||||
}
|
||||
|
||||
@ -77,12 +87,7 @@ func (h *RedPacketHandler) ClaimSign(c *gin.Context) {
|
||||
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)
|
||||
result, err := h.rpSvc.IssueClaimSign(c.Request.Context(), req.PacketID, req.Claimer, req.RandomSeed)
|
||||
if err != nil {
|
||||
resp.InternalError(c, "failed to issue claim signature: "+err.Error())
|
||||
return
|
||||
@ -92,6 +97,11 @@ func (h *RedPacketHandler) ClaimSign(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) ClaimResult(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req service.ClaimResultRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
@ -105,3 +115,62 @@ func (h *RedPacketHandler) ClaimResult(c *gin.Context) {
|
||||
|
||||
resp.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) WalletBindChallenge(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req service.WalletBindChallengeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.IssueWalletBindChallenge(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) WalletBindConfirm(c *gin.Context) {
|
||||
var req service.WalletBindConfirmRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.ConfirmWalletBind(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *RedPacketHandler) WalletBindDetail(c *gin.Context) {
|
||||
if err := authctx.BindCurrentUserID(c); err != nil {
|
||||
resp.Forbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
chainType := c.Query("chain_type")
|
||||
walletAddress := c.Query("wallet_address")
|
||||
if chainType == "" || walletAddress == "" {
|
||||
resp.BadRequest(c, "chain_type and wallet_address are required")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.rpSvc.GetWalletBinding(c.Request.Context(), "", chainType, walletAddress)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusNotFound, 404, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.OK(c, result)
|
||||
}
|
||||
|
||||
@ -7,15 +7,22 @@ import (
|
||||
type RedPacket struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
BizID string `gorm:"uniqueIndex;size:64" json:"biz_id"`
|
||||
ChainType string `gorm:"index;size:16" json:"chain_type"` // EVM, TRON
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
ContractAddress string `json:"contract_address"`
|
||||
CreatorUserID string `gorm:"size:64" json:"creator_user_id"`
|
||||
CreatorWallet string `gorm:"size:66" json:"creator_wallet"`
|
||||
GroupID string `gorm:"index;size:64" json:"group_id"`
|
||||
ScopeType string `gorm:"size:20" json:"scope_type"` // GROUP, DIRECT, PUBLIC
|
||||
ReceiverUserID string `gorm:"size:64" json:"receiver_user_id"`
|
||||
ReceiverUserIDs string `gorm:"type:text" json:"receiver_user_ids"`
|
||||
PacketType int32 `json:"packet_type"` // 0=fixed, 1=random, 2=transfer
|
||||
Token string `gorm:"size:66" json:"token"`
|
||||
TotalAmount string `gorm:"size:50" json:"total_amount"`
|
||||
TotalShares int32 `json:"total_shares"`
|
||||
ClaimedAmount string `gorm:"size:50" json:"claimed_amount"`
|
||||
ClaimedShares int32 `json:"claimed_shares"`
|
||||
ExpiryAt int64 `json:"expiry_at"`
|
||||
TxHash string `gorm:"size:66" json:"tx_hash"`
|
||||
Status string `gorm:"size:20" json:"status"` // PENDING, ACTIVE, EXPIRED, COMPLETED, REFUNDED
|
||||
@ -24,35 +31,69 @@ type RedPacket struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;index:idx_packet_user;size:32" json:"packet_id"`
|
||||
UserID string `gorm:"index;index:idx_packet_user;size:64" json:"user_id"`
|
||||
ClaimerWallet string `gorm:"size:66" json:"claimer_wallet"`
|
||||
AuthNonce string `gorm:"size:32" json:"auth_nonce"`
|
||||
ClaimTxHash string `gorm:"uniqueIndex;size:66" json:"claim_tx_hash"`
|
||||
ClaimedAmount string `gorm:"size:50" json:"claimed_amount"`
|
||||
BlockNumber uint64 `json:"block_number"`
|
||||
Status string `gorm:"size:20" json:"status"` // PENDING, CONFIRMED, FAILED
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RedPacketClaimAuth struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
Claimer string `gorm:"size:66" json:"claimer"`
|
||||
AuthNonce string `gorm:"uniqueIndex;size:32" json:"auth_nonce"`
|
||||
RandomSeed string `gorm:"size:32" json:"random_seed"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
Signature string `gorm:"size:132" json:"signature"`
|
||||
Used bool `json:"used"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
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"`
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PacketID string `gorm:"index;size:32" json:"packet_id"`
|
||||
RefundTo string `gorm:"size:66" json:"refund_to"`
|
||||
TxHash string `gorm:"uniqueIndex;size:66" json:"tx_hash"`
|
||||
Amount string `gorm:"size:50" json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type WalletBindingChallenge struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
ChallengeID string `gorm:"uniqueIndex;size:64" json:"challenge_id"`
|
||||
UserID string `gorm:"index;size:64" json:"user_id"`
|
||||
ChainType string `gorm:"index;size:16" json:"chain_type"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
WalletAddress string `gorm:"index;size:128" json:"wallet_address"`
|
||||
Nonce string `gorm:"size:64" json:"nonce"`
|
||||
Message string `gorm:"type:text" json:"message"`
|
||||
Protocol string `gorm:"size:32" json:"protocol"`
|
||||
SignMethod string `gorm:"size:32" json:"sign_method"`
|
||||
Status string `gorm:"size:20" json:"status"` // PENDING, VERIFIED, EXPIRED, FAILED
|
||||
Signature string `gorm:"type:text" json:"signature"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type WalletBinding struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID string `gorm:"index:idx_user_chain_wallet,unique;size:64" json:"user_id"`
|
||||
ChainType string `gorm:"index:idx_user_chain_wallet,unique;size:16" json:"chain_type"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
WalletAddress string `gorm:"index:idx_user_chain_wallet,unique;size:128" json:"wallet_address"`
|
||||
Status string `gorm:"size:20" json:"status"` // ACTIVE, REVOKED
|
||||
ChallengeID string `gorm:"size:64" json:"challenge_id"`
|
||||
VerifiedAt time.Time `json:"verified_at"`
|
||||
RevokedAt *time.Time `json:"revoked_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@ -2,22 +2,34 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"redpacket/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
CreateRedPacket(ctx context.Context, rp *model.RedPacket) error
|
||||
GetRedPacketByBizID(ctx context.Context, bizID string) (*model.RedPacket, error)
|
||||
GetRedPacketByPacketID(ctx context.Context, packetID string) (*model.RedPacket, error)
|
||||
UpdateRedPacketTxHash(ctx context.Context, bizID, txHash, packetID string) error
|
||||
UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error
|
||||
UpdateRedPacketStatus(ctx context.Context, packetID, status string) error
|
||||
UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error
|
||||
CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error
|
||||
GetClaimAuth(ctx context.Context, packetID, claimer string) (*model.RedPacketClaimAuth, error)
|
||||
MarkClaimAuthUsed(ctx context.Context, authNonce string) error
|
||||
CreateClaim(ctx context.Context, claim *model.RedPacketClaim) error
|
||||
GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error)
|
||||
GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error)
|
||||
SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error
|
||||
GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, error)
|
||||
SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error
|
||||
CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
|
||||
GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error)
|
||||
UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error
|
||||
UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error
|
||||
GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
@ -44,16 +56,54 @@ func (r *repository) GetRedPacketByPacketID(ctx context.Context, packetID string
|
||||
return &rp, err
|
||||
}
|
||||
|
||||
func (r *repository) UpdateRedPacketTxHash(ctx context.Context, bizID, txHash, packetID string) error {
|
||||
func (r *repository) UpdateRedPacketCreated(ctx context.Context, rp *model.RedPacket) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacket{}).
|
||||
Where("biz_id = ?", bizID).
|
||||
Where("biz_id = ?", rp.BizID).
|
||||
Updates(map[string]interface{}{
|
||||
"tx_hash": txHash,
|
||||
"packet_id": packetID,
|
||||
"status": "ACTIVE",
|
||||
"chain_type": rp.ChainType,
|
||||
"packet_id": rp.PacketID,
|
||||
"tx_hash": rp.TxHash,
|
||||
"chain_id": rp.ChainID,
|
||||
"contract_address": rp.ContractAddress,
|
||||
"group_id": rp.GroupID,
|
||||
"scope_type": rp.ScopeType,
|
||||
"receiver_user_id": rp.ReceiverUserID,
|
||||
"receiver_user_ids": rp.ReceiverUserIDs,
|
||||
"status": rp.Status,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateRedPacketStatus(ctx context.Context, packetID, status string) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacket{}).
|
||||
Where("packet_id = ?", packetID).
|
||||
Update("status", status).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateRedPacketClaimProgress(ctx context.Context, packetID, claimedAmount, status string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var rp model.RedPacket
|
||||
if err := tx.Where("packet_id = ?", packetID).First(&rp).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalClaimed := addNumericStrings(rp.ClaimedAmount, claimedAmount)
|
||||
nextShares := rp.ClaimedShares + 1
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"claimed_amount": totalClaimed,
|
||||
"claimed_shares": nextShares,
|
||||
"updated_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||
}
|
||||
if status != "" {
|
||||
updates["status"] = status
|
||||
}
|
||||
|
||||
return tx.Model(&model.RedPacket{}).
|
||||
Where("id = ?", rp.ID).
|
||||
Updates(updates).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *repository) CreateClaimAuth(ctx context.Context, auth *model.RedPacketClaimAuth) error {
|
||||
return r.db.WithContext(ctx).Create(auth).Error
|
||||
}
|
||||
@ -70,8 +120,62 @@ func (r *repository) MarkClaimAuthUsed(ctx context.Context, authNonce string) er
|
||||
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) GetClaimByPacketIDAndClaimer(ctx context.Context, packetID, claimer string) (*model.RedPacketClaim, error) {
|
||||
var claim model.RedPacketClaim
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("packet_id = ? AND claimer_wallet = ?", packetID, claimer).
|
||||
Order("created_at desc").
|
||||
First(&claim).Error
|
||||
return &claim, err
|
||||
}
|
||||
|
||||
func (r *repository) GetClaimByPacketIDAndUserID(ctx context.Context, packetID, userID string) (*model.RedPacketClaim, error) {
|
||||
var claim model.RedPacketClaim
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("packet_id = ? AND user_id = ?", packetID, userID).
|
||||
Order("created_at desc").
|
||||
First(&claim).Error
|
||||
return &claim, err
|
||||
}
|
||||
|
||||
func (r *repository) SaveClaim(ctx context.Context, claim *model.RedPacketClaim) error {
|
||||
if claim.UserID != "" {
|
||||
var existing model.RedPacketClaim
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("packet_id = ? AND user_id = ?", claim.PacketID, claim.UserID).
|
||||
First(&existing).Error
|
||||
if err == nil {
|
||||
claim.ID = existing.ID
|
||||
return r.db.WithContext(ctx).Model(&model.RedPacketClaim{}).
|
||||
Where("id = ?", existing.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"claimer_wallet": existing.ClaimerWallet,
|
||||
"auth_nonce": claim.AuthNonce,
|
||||
"claim_tx_hash": claim.ClaimTxHash,
|
||||
"claimed_amount": claim.ClaimedAmount,
|
||||
"block_number": claim.BlockNumber,
|
||||
"status": claim.Status,
|
||||
"updated_at": claim.UpdatedAt,
|
||||
}).Error
|
||||
}
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "claim_tx_hash"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"user_id",
|
||||
"packet_id",
|
||||
"claimer_wallet",
|
||||
"auth_nonce",
|
||||
"claimed_amount",
|
||||
"block_number",
|
||||
"status",
|
||||
"updated_at",
|
||||
}),
|
||||
}).Create(claim).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetClaimsByPacketID(ctx context.Context, packetID string) ([]model.RedPacketClaim, error) {
|
||||
@ -79,3 +183,69 @@ func (r *repository) GetClaimsByPacketID(ctx context.Context, packetID string) (
|
||||
err := r.db.WithContext(ctx).Where("packet_id = ?", packetID).Order("created_at desc").Find(&claims).Error
|
||||
return claims, err
|
||||
}
|
||||
|
||||
func (r *repository) SaveRefund(ctx context.Context, refund *model.RedPacketRefund) error {
|
||||
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "tx_hash"}},
|
||||
DoNothing: true,
|
||||
}).Create(refund).Error
|
||||
}
|
||||
|
||||
func (r *repository) CreateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
|
||||
return r.db.WithContext(ctx).Create(challenge).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetWalletBindingChallenge(ctx context.Context, challengeID string) (*model.WalletBindingChallenge, error) {
|
||||
var challenge model.WalletBindingChallenge
|
||||
err := r.db.WithContext(ctx).Where("challenge_id = ?", challengeID).First(&challenge).Error
|
||||
return &challenge, err
|
||||
}
|
||||
|
||||
func (r *repository) UpdateWalletBindingChallenge(ctx context.Context, challenge *model.WalletBindingChallenge) error {
|
||||
return r.db.WithContext(ctx).Model(&model.WalletBindingChallenge{}).
|
||||
Where("challenge_id = ?", challenge.ChallengeID).
|
||||
Updates(map[string]interface{}{
|
||||
"status": challenge.Status,
|
||||
"signature": challenge.Signature,
|
||||
"verified_at": challenge.VerifiedAt,
|
||||
"updated_at": challenge.UpdatedAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpsertWalletBinding(ctx context.Context, binding *model.WalletBinding) error {
|
||||
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "user_id"},
|
||||
{Name: "chain_type"},
|
||||
{Name: "wallet_address"},
|
||||
},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"chain_id",
|
||||
"status",
|
||||
"challenge_id",
|
||||
"verified_at",
|
||||
"revoked_at",
|
||||
"updated_at",
|
||||
}),
|
||||
}).Create(binding).Error
|
||||
}
|
||||
|
||||
func (r *repository) GetActiveWalletBinding(ctx context.Context, userID, chainType, walletAddress string) (*model.WalletBinding, error) {
|
||||
var binding model.WalletBinding
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("user_id = ? AND chain_type = ? AND wallet_address = ? AND status = ?", userID, chainType, walletAddress, "ACTIVE").
|
||||
First(&binding).Error
|
||||
return &binding, err
|
||||
}
|
||||
|
||||
func addNumericStrings(current, delta string) string {
|
||||
left := new(big.Int)
|
||||
if current != "" {
|
||||
left.SetString(current, 10)
|
||||
}
|
||||
right := new(big.Int)
|
||||
if delta != "" {
|
||||
right.SetString(delta, 10)
|
||||
}
|
||||
return new(big.Int).Add(left, right).String()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,386 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"redpacket/internal/authctx"
|
||||
"redpacket/internal/model"
|
||||
"redpacket/internal/repository"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newTestService(t *testing.T) (*RedPacketService, repository.Repository) {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("gorm.Open() error = %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&model.RedPacket{},
|
||||
&model.RedPacketClaim{},
|
||||
&model.RedPacketClaimAuth{},
|
||||
&model.RedPacketRefund{},
|
||||
&model.WalletBindingChallenge{},
|
||||
&model.WalletBinding{},
|
||||
); err != nil {
|
||||
t.Fatalf("AutoMigrate() error = %v", err)
|
||||
}
|
||||
|
||||
repo := repository.New(db)
|
||||
svc := NewRedPacketService(repo, nil, nil, "")
|
||||
return svc, repo
|
||||
}
|
||||
|
||||
func seedWalletBinding(t *testing.T, repo repository.Repository, userID, chainType, wallet string) {
|
||||
t.Helper()
|
||||
|
||||
err := repo.UpsertWalletBinding(context.Background(), &model.WalletBinding{
|
||||
UserID: userID,
|
||||
ChainType: chainType,
|
||||
WalletAddress: wallet,
|
||||
Status: "ACTIVE",
|
||||
ChallengeID: "test-challenge",
|
||||
VerifiedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertWalletBinding() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanClaimRejectsExpiredAndAlreadyClaimed(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
activePacket := &model.RedPacket{
|
||||
BizID: "biz-active",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1001",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-active",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, activePacket); err != nil {
|
||||
t.Fatalf("CreateRedPacket(active) error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0xclaimer")
|
||||
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: "1001",
|
||||
ClaimerWallet: "0xclaimer",
|
||||
ClaimTxHash: "0xtx1",
|
||||
Status: "CONFIRMED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.SaveClaim(ctx, claim); err != nil {
|
||||
t.Fatalf("SaveClaim() error = %v", err)
|
||||
}
|
||||
|
||||
if err := svc.CanClaim(ctx, "1001", "0xclaimer", "u2"); err == nil || err.Error() != "already claimed" {
|
||||
t.Fatalf("expected already claimed error, got %v", err)
|
||||
}
|
||||
|
||||
expiredPacket := &model.RedPacket{
|
||||
BizID: "biz-expired",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1002",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-expired",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(-1 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, expiredPacket); err != nil {
|
||||
t.Fatalf("CreateRedPacket(expired) error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u3", "EVM", "0xfresh")
|
||||
|
||||
if err := svc.CanClaim(authctx.WithCurrentUserID(context.Background(), "u3"), "1002", "0xfresh", "u3"); err == nil || err.Error() != "packet is expired" {
|
||||
t.Fatalf("expected expired error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanClaimRejectsAlreadyClaimedByUserID(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
packet := &model.RedPacket{
|
||||
BizID: "biz-user-claimed",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1003",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-user-claimed",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, packet); err != nil {
|
||||
t.Fatalf("CreateRedPacket() error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0xanother-wallet")
|
||||
|
||||
claim := &model.RedPacketClaim{
|
||||
PacketID: "1003",
|
||||
UserID: "u2",
|
||||
ClaimerWallet: "0xclaimer",
|
||||
ClaimTxHash: "0xtx-user-claimed",
|
||||
Status: "CONFIRMED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.SaveClaim(ctx, claim); err != nil {
|
||||
t.Fatalf("SaveClaim() error = %v", err)
|
||||
}
|
||||
|
||||
if err := svc.CanClaim(ctx, "1003", "0xanother-wallet", "u2"); err == nil || err.Error() != "user already claimed" {
|
||||
t.Fatalf("expected user already claimed error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanClaimUsesPacketTypeRules(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
groupPacket := &model.RedPacket{
|
||||
BizID: "biz-group",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1101",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
PacketType: 0,
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, groupPacket); err != nil {
|
||||
t.Fatalf("CreateRedPacket(group) error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0xclaimer")
|
||||
if err := svc.CanClaim(ctx, "1101", "0xclaimer", "u2"); err == nil || err.Error() != "group_id is required for fixed packet claim" {
|
||||
t.Fatalf("expected missing group_id error, got %v", err)
|
||||
}
|
||||
|
||||
transferPacket := &model.RedPacket{
|
||||
BizID: "biz-transfer",
|
||||
ChainType: "EVM",
|
||||
PacketID: "1102",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
PacketType: 2,
|
||||
ReceiverUserID: "u-receiver",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, transferPacket); err != nil {
|
||||
t.Fatalf("CreateRedPacket(transfer) error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u-other", "EVM", "0xclaimer")
|
||||
if err := svc.CanClaim(ctx, "1102", "0xclaimer", "u-other"); err == nil || err.Error() != "user is not the designated receiver" {
|
||||
t.Fatalf("expected designated receiver error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrderPersistsScopeFields(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u-create")
|
||||
|
||||
result, err := svc.CreateOrder(ctx, &CreateOrderRequest{
|
||||
ChainType: "EVM",
|
||||
CreatorWallet: "0x1111111111111111111111111111111111111111",
|
||||
GroupID: "g-100",
|
||||
ScopeType: "group",
|
||||
PacketType: 1,
|
||||
Token: "0x2222222222222222222222222222222222222222",
|
||||
TotalAmount: "1000",
|
||||
TotalShares: 10,
|
||||
ReceiverUserIDs: []string{"u2", "u3"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOrder() error = %v", err)
|
||||
}
|
||||
|
||||
bizID, _ := result["biz_id"].(string)
|
||||
record, err := repo.GetRedPacketByBizID(ctx, bizID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRedPacketByBizID() error = %v", err)
|
||||
}
|
||||
|
||||
if record.ScopeType != "GROUP" {
|
||||
t.Fatalf("scope type mismatch: got %s", record.ScopeType)
|
||||
}
|
||||
if record.ChainType != "EVM" {
|
||||
t.Fatalf("chain type mismatch: got %s", record.ChainType)
|
||||
}
|
||||
if record.GroupID != "g-100" {
|
||||
t.Fatalf("group id mismatch: got %s", record.GroupID)
|
||||
}
|
||||
|
||||
var got []string
|
||||
if err := json.Unmarshal([]byte(record.ReceiverUserIDs), &got); err != nil {
|
||||
t.Fatalf("Unmarshal(receiver_user_ids) error = %v", err)
|
||||
}
|
||||
if len(got) != 2 || got[0] != "u2" || got[1] != "u3" {
|
||||
t.Fatalf("receiver_user_ids mismatch: got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatedCallbackUpdatesBindingAndScope(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u-create")
|
||||
|
||||
result, err := svc.CreateOrder(ctx, &CreateOrderRequest{
|
||||
ChainType: "TRON",
|
||||
CreatorWallet: "0x1111111111111111111111111111111111111111",
|
||||
PacketType: 2,
|
||||
Token: "0x0000000000000000000000000000000000000000",
|
||||
TotalAmount: "1000",
|
||||
TotalShares: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOrder() error = %v", err)
|
||||
}
|
||||
|
||||
bizID, _ := result["biz_id"].(string)
|
||||
err = svc.CreatedCallback(ctx, &CreatedCallbackRequest{
|
||||
BizID: bizID,
|
||||
TxHash: "0xabc123",
|
||||
PacketID: "3001",
|
||||
ScopeType: "DIRECT",
|
||||
ReceiverUserID: "u-receiver",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatedCallback() error = %v", err)
|
||||
}
|
||||
|
||||
record, err := repo.GetRedPacketByBizID(ctx, bizID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRedPacketByBizID() error = %v", err)
|
||||
}
|
||||
|
||||
if record.PacketID != "3001" {
|
||||
t.Fatalf("packet id mismatch: got %s", record.PacketID)
|
||||
}
|
||||
if record.ChainType != "TRON" {
|
||||
t.Fatalf("chain type mismatch: got %s", record.ChainType)
|
||||
}
|
||||
if record.TxHash != "0xabc123" {
|
||||
t.Fatalf("tx hash mismatch: got %s", record.TxHash)
|
||||
}
|
||||
if record.Status != "ACTIVE" {
|
||||
t.Fatalf("status mismatch: got %s", record.Status)
|
||||
}
|
||||
if record.ScopeType != "DIRECT" {
|
||||
t.Fatalf("scope type mismatch: got %s", record.ScopeType)
|
||||
}
|
||||
if record.ReceiverUserID != "u-receiver" {
|
||||
t.Fatalf("receiver user mismatch: got %s", record.ReceiverUserID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueClaimSignValidatesInputsAndPersistsAuth(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
packet := &model.RedPacket{
|
||||
BizID: "biz-sign",
|
||||
ChainType: "EVM",
|
||||
PacketID: "2001",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-sign",
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, packet); err != nil {
|
||||
t.Fatalf("CreateRedPacket() error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0x1111111111111111111111111111111111111111")
|
||||
|
||||
if _, err := svc.IssueClaimSign(ctx, "bad-packet-id", "0x1111111111111111111111111111111111111111", "0"); err == nil {
|
||||
t.Fatalf("expected invalid packet id error")
|
||||
}
|
||||
|
||||
result, err := svc.IssueClaimSign(ctx, "2001", "0x1111111111111111111111111111111111111111", "123")
|
||||
if err != nil {
|
||||
t.Fatalf("IssueClaimSign() error = %v", err)
|
||||
}
|
||||
|
||||
auth, err := repo.GetClaimAuth(ctx, "2001", "0x1111111111111111111111111111111111111111")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClaimAuth() error = %v", err)
|
||||
}
|
||||
if auth.AuthNonce == "" {
|
||||
t.Fatalf("expected auth nonce to be persisted")
|
||||
}
|
||||
if auth.RandomSeed != "123" {
|
||||
t.Fatalf("random seed mismatch: got %s", auth.RandomSeed)
|
||||
}
|
||||
if result["auth_nonce"] == "" {
|
||||
t.Fatalf("expected auth_nonce in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimResultPersistsPendingWithoutChainParser(t *testing.T) {
|
||||
svc, repo := newTestService(t)
|
||||
ctx := authctx.WithCurrentUserID(context.Background(), "u2")
|
||||
|
||||
packet := &model.RedPacket{
|
||||
BizID: "biz-claim-result",
|
||||
ChainType: "EVM",
|
||||
PacketID: "2101",
|
||||
CreatorUserID: "u1",
|
||||
CreatorWallet: "0xabc",
|
||||
GroupID: "g-1",
|
||||
PacketType: 0,
|
||||
Status: "ACTIVE",
|
||||
ExpiryAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := repo.CreateRedPacket(ctx, packet); err != nil {
|
||||
t.Fatalf("CreateRedPacket() error = %v", err)
|
||||
}
|
||||
seedWalletBinding(t, repo, "u2", "EVM", "0x1111111111111111111111111111111111111111")
|
||||
|
||||
err := svc.ClaimResult(ctx, &ClaimResultRequest{
|
||||
PacketID: "2101",
|
||||
Claimer: "0x1111111111111111111111111111111111111111",
|
||||
TxHash: "0xtx-claim",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimResult() error = %v", err)
|
||||
}
|
||||
|
||||
claim, err := repo.GetClaimByPacketIDAndClaimer(ctx, "2101", "0x1111111111111111111111111111111111111111")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClaimByPacketIDAndClaimer() error = %v", err)
|
||||
}
|
||||
if claim.Status != "PENDING" {
|
||||
t.Fatalf("claim status mismatch: got %s", claim.Status)
|
||||
}
|
||||
if claim.UserID != "u2" {
|
||||
t.Fatalf("user id mismatch: got %s", claim.UserID)
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,8 @@ func main() {
|
||||
&model.RedPacketClaim{},
|
||||
&model.RedPacketClaimAuth{},
|
||||
&model.RedPacketRefund{},
|
||||
&model.WalletBindingChallenge{},
|
||||
&model.WalletBinding{},
|
||||
); err != nil {
|
||||
log.Fatalf("failed to auto-migrate: %v", err)
|
||||
}
|
||||
@ -63,10 +65,6 @@ func main() {
|
||||
// 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 != "" {
|
||||
@ -91,6 +89,10 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create repository and service
|
||||
repo := repository.New(db)
|
||||
rpSvc := service.NewRedPacketService(repo, chainClient, tronClient, cfg.Chain.SignerPrivateKey)
|
||||
|
||||
// Create admin service and handler
|
||||
adminSvc := service.NewAdminService(chainClient, tronClient)
|
||||
adminHandler := handler.NewAdminHandler(adminSvc)
|
||||
|
||||
@ -19,6 +19,9 @@ func Setup(r *gin.Engine, rpHandler *handler.RedPacketHandler, adminHandler *han
|
||||
api.GET("/detail", rpHandler.Detail)
|
||||
api.POST("/claim-sign", rpHandler.ClaimSign)
|
||||
api.POST("/claim-result", rpHandler.ClaimResult)
|
||||
api.POST("/wallet-bind/challenge", rpHandler.WalletBindChallenge)
|
||||
api.POST("/wallet-bind/confirm", rpHandler.WalletBindConfirm)
|
||||
api.GET("/wallet-bind/detail", rpHandler.WalletBindDetail)
|
||||
}
|
||||
|
||||
// Admin APIs - should be protected with authentication in production
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user