mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-06-23 09:35:37 +08:00
Merge branch 'feature/redpacket_new1' into develop/tom
# Conflicts: # config/openim-rpc-redpacket.yml # internal/rpc/redpacket/redpacket.go
This commit is contained in:
commit
8079cb3eb3
@ -1,8 +1,9 @@
|
||||
# RedPacket 前端对接文档
|
||||
|
||||
本文档面向前端 / 网关 / App 对接方,说明红包领取和钱包绑定的真实接入方式,重点覆盖:
|
||||
本文档面向前端 / 网关 / App 对接方,说明红包创建、领取和钱包绑定的真实接入方式,重点覆盖:
|
||||
|
||||
- 如何把当前登录用户传递给红包服务
|
||||
- 如何创建红包(业务单 + 链上创建 + 回写激活)
|
||||
- 如何绑定钱包
|
||||
- 如何申请领取签名
|
||||
- 前端何时发链、何时回写后端
|
||||
@ -97,7 +98,111 @@ Content-Type: application/json
|
||||
|
||||
已经在后端建立了有效绑定关系。
|
||||
|
||||
## 3. 领取签名流程
|
||||
## 3. 创建红包流程
|
||||
|
||||
### 4.1 流程图
|
||||
|
||||
```text
|
||||
前端 -> 红包服务: POST /api/redpacket/create-order
|
||||
红包服务 -> 前端: biz_id (状态 PENDING)
|
||||
前端 -> 钱包/链上: createFixedPacket/createRandomPacket/createTransfer
|
||||
链上 -> 前端: tx_hash + packet_id(从事件或回执解析)
|
||||
前端 -> 红包服务: POST /api/redpacket/created-callback
|
||||
红包服务 -> 红包服务: 校验创建参数并激活红包
|
||||
红包服务 -> 前端: 回写成功 (状态 ACTIVE)
|
||||
前端 -> 红包服务: POST /api/redpacket/detail (可选)
|
||||
```
|
||||
|
||||
### 3.2 创建业务单(发链前必调)
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
POST /api/redpacket/create-order
|
||||
token: <user token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"chain_type": "EVM",
|
||||
"chain_id": 1,
|
||||
"contract_address": "0xA1f42567559aBA5Ff0aac84cdE1AaF1F9DbB888F",
|
||||
"creator_wallet": "0x1111111111111111111111111111111111111111",
|
||||
"group_id": "g001",
|
||||
"scope_type": "GROUP",
|
||||
"receiver_user_id": "",
|
||||
"receiver_user_ids": [],
|
||||
"packet_type": 1,
|
||||
"token": "0x2222222222222222222222222222222222222222",
|
||||
"total_amount": "1000000000000000000",
|
||||
"total_shares": 10,
|
||||
"expiry_at": 0,
|
||||
"remark": "happy new year"
|
||||
}
|
||||
```
|
||||
|
||||
关键说明:
|
||||
|
||||
- 不需要传 `user_id`,创建人从上下文 `opUserID` 取
|
||||
- `total_amount` 必须是链上最小单位十进制字符串(例如 wei)
|
||||
- `packet_type`: `0` 固定红包,`1` 拼手气红包,`2` 转账
|
||||
- `scope_type=GROUP` 时必须传 `group_id`
|
||||
- `scope_type=DIRECT` 时必须传 `receiver_user_id` 或 `receiver_user_ids`
|
||||
|
||||
成功响应里最关键的是:
|
||||
|
||||
- `biz_id`: 业务红包单号(后续回写必须带上)
|
||||
|
||||
### 3.3 链上创建红包
|
||||
|
||||
前端拿到 `biz_id` 后,再调用链上创建方法:
|
||||
|
||||
- 固定红包:`createFixedPacket(...)`
|
||||
- 拼手气红包:`createRandomPacket(...)`
|
||||
- 转账红包:`createTransfer(...)`
|
||||
|
||||
链上交易成功后,前端需要得到:
|
||||
|
||||
- `tx_hash`
|
||||
- `packet_id`(优先从 `PacketCreated` 事件解析)
|
||||
|
||||
### 3.4 创建回写(激活红包)
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
POST /api/redpacket/created-callback
|
||||
token: <user token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"biz_id": "f8a0f87e-d9cb-4d4a-8350-7bd43ab2e9a4",
|
||||
"tx_hash": "0xabc123...",
|
||||
"packet_id": "10001",
|
||||
"group_id": "g001",
|
||||
"scope_type": "GROUP",
|
||||
"receiver_user_id": "",
|
||||
"receiver_user_ids": []
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `biz_id`、`tx_hash` 必填
|
||||
- 推荐传 `packet_id`(可减少后端 fallback 分支)
|
||||
- 回写成功后红包状态从 `PENDING` 变为 `ACTIVE`
|
||||
- 回写后可调 `/api/redpacket/detail` 刷新页面状态
|
||||
|
||||
### 3.5 创建流程常见坑
|
||||
|
||||
- 先发链再 `create_order`:会导致回写阶段缺少有效 `biz_id`
|
||||
- `create_order` 的 `creator_wallet` 与实际发链钱包不一致:可能被后端校验拦截
|
||||
- 未调用 `created_callback`:红包会一直停留在 `PENDING`,领取侧会失败
|
||||
|
||||
## 4. 领取签名流程
|
||||
|
||||
### 3.1 流程图
|
||||
|
||||
@ -111,7 +216,7 @@ Content-Type: application/json
|
||||
链监听器 -> 红包服务: 最终确认领取结果
|
||||
```
|
||||
|
||||
### 3.2 申请领取签名
|
||||
### 4.2 申请领取签名
|
||||
|
||||
请求:
|
||||
|
||||
@ -159,7 +264,7 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 前端拿到响应后要做什么
|
||||
### 4.3 前端拿到响应后要做什么
|
||||
|
||||
前端必须原样把这些参数传给链上:
|
||||
|
||||
@ -182,7 +287,7 @@ claim(packetId, authNonce, randomSeed, deadline, signature)
|
||||
- 不要对摘要再次做 `signMessage`
|
||||
- 后端返回的 `signature` 已经是最终可上链签名
|
||||
|
||||
## 4. 领取结果回写
|
||||
## 5. 领取结果回写
|
||||
|
||||
`claim-result` 是可选的,主要作用是让业务侧尽快看到一条 `PENDING` 领取记录。
|
||||
|
||||
@ -210,29 +315,38 @@ Content-Type: application/json
|
||||
- 如果不能,会先记成 `PENDING`
|
||||
- 最终仍以链监听器为准
|
||||
|
||||
## 5. 前端推荐调用顺序
|
||||
## 6. 前端推荐调用顺序
|
||||
|
||||
### 5.1 首次使用钱包领取
|
||||
### 6.1 创建红包
|
||||
|
||||
1. 用户登录业务系统
|
||||
2. 前端请求 `/wallet-bind/challenge`
|
||||
2. 前端请求 `/api/redpacket/create-order`
|
||||
3. 拿到 `biz_id` 后,钱包调用链上创建红包方法
|
||||
4. 从交易回执/事件拿到 `tx_hash`、`packet_id`
|
||||
5. 前端请求 `/api/redpacket/created-callback`
|
||||
6. 前端请求 `/api/redpacket/detail` 刷新状态(确认 `ACTIVE`)
|
||||
|
||||
### 6.2 首次使用钱包领取
|
||||
|
||||
1. 用户登录业务系统
|
||||
2. 前端请求 `/api/redpacket/wallet-bind/challenge`
|
||||
3. 钱包对 `message` 签名
|
||||
4. 前端请求 `/wallet-bind/confirm`
|
||||
4. 前端请求 `/api/redpacket/wallet-bind/confirm`
|
||||
5. 绑定成功后再进入领取流程
|
||||
|
||||
### 5.2 正常领取
|
||||
### 6.3 正常领取
|
||||
|
||||
1. 前端拿到红包 `packet_id`
|
||||
2. 用户连接钱包,得到本次 `claimer` 地址
|
||||
3. 前端请求 `/claim-sign`
|
||||
3. 前端请求 `/api/redpacket/claim-sign`
|
||||
4. 拿到 `auth_nonce + random_seed + deadline + signature`
|
||||
5. 前端调用链上 `claim(...)`
|
||||
6. 前端可选请求 `/claim-result`
|
||||
6. 前端可选请求 `/api/redpacket/claim-result`
|
||||
7. 页面轮询详情页或等待业务侧状态同步
|
||||
|
||||
## 6. 常见错误和排查
|
||||
## 7. 常见错误和排查
|
||||
|
||||
### 6.1 `op user id missing in context`
|
||||
### 7.1 `op user id missing in context`
|
||||
|
||||
原因:
|
||||
|
||||
@ -240,7 +354,7 @@ Content-Type: application/json
|
||||
- 网关没有把 `opUserID` 注入上下文
|
||||
- 直接绕过网关调用了红包服务
|
||||
|
||||
### 6.2 `wallet is not bound to user`
|
||||
### 7.2 `wallet is not bound to user`
|
||||
|
||||
原因:
|
||||
|
||||
@ -248,20 +362,20 @@ Content-Type: application/json
|
||||
- 当前钱包绑定的是别的业务用户
|
||||
- 链类型不一致
|
||||
|
||||
### 6.3 `already claimed`
|
||||
### 7.3 `already claimed`
|
||||
|
||||
原因:
|
||||
|
||||
- 同一个钱包地址已经领过该红包
|
||||
|
||||
### 6.4 `user already claimed`
|
||||
### 7.4 `user already claimed`
|
||||
|
||||
原因:
|
||||
|
||||
- 同一个业务用户已经领取过该红包
|
||||
- 即使换钱包地址,也会被后端拦截
|
||||
|
||||
## 7. 后端接口与代码位置
|
||||
## 8. 后端接口与代码位置
|
||||
|
||||
- 接口契约文档:
|
||||
[backend-api.md](/Users/panda/aiCode/red_packet/open-im-server-origin/cmd/openim-rpc/openim-rpc-redpacket/backend-api.md)
|
||||
|
||||
@ -12,11 +12,11 @@ prometheus:
|
||||
# Leave rpcURL empty to disable the EVM client; the RPC service will then
|
||||
# only expose TRON-related functionality (or the offchain code paths).
|
||||
chain:
|
||||
rpcURL: ""
|
||||
contractAddress: ""
|
||||
chainID: 0
|
||||
signerPrivateKey: ""
|
||||
configAdminPrivateKey: ""
|
||||
rpcURL: "https://data-seed-prebsc-1-s1.bnbchain.org:8545"
|
||||
contractAddress: "0x9f2e22F5D0cf8d8127E319D38b3EDDaE43bb4DC0"
|
||||
chainID: 97
|
||||
signerPrivateKey: "e9f6a5f3a3c3a97099ca31e7f44151e529c0a4f8a91d5d4232c7282f2b798df4"
|
||||
configAdminPrivateKey: "e9f6a5f3a3c3a97099ca31e7f44151e529c0a4f8a91d5d4232c7282f2b798df4"
|
||||
|
||||
# TRON full-node configuration. Leave fullNodeURL empty to disable TRON.
|
||||
tron:
|
||||
@ -27,7 +27,8 @@ tron:
|
||||
feeLimit: 100000000
|
||||
|
||||
# Indexer polling interval (in seconds). Used by both EVM and TRON event indexers.
|
||||
# Set to 0 or negative to disable block scanning completely and rely on tx-hash parsing paths.
|
||||
indexer:
|
||||
pollInterval: 5
|
||||
pollInterval: 0
|
||||
# EVM only: max block span per eth_getLogs request (0 = default 2000). Increase if your node allows larger ranges.
|
||||
maxBlocksPerPoll: 2000
|
||||
|
||||
@ -173,7 +173,7 @@ func (s *redPacketServer) ParseTxEvents(ctx context.Context, req *pbredpacket.Pa
|
||||
if s.tronClient == nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("TRON client not configured")
|
||||
}
|
||||
events, err := s.tronClient.ParseTransactionReceipt(ctx, req.TxHash)
|
||||
success, events, err := s.tronClient.ParseTransactionReceiptWithStatus(ctx, req.TxHash)
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("parse TRON tx receipt failed: " + err.Error())
|
||||
}
|
||||
@ -185,12 +185,16 @@ func (s *redPacketServer) ParseTxEvents(ctx context.Context, req *pbredpacket.Pa
|
||||
}
|
||||
out = append(out, &pbredpacket.ParsedEvent{Name: e.Name, Data: data})
|
||||
}
|
||||
return &pbredpacket.ParseTxEventsResp{Chain: "tron", TxHash: req.TxHash, Events: out}, nil
|
||||
note := "tx_status=FAILED"
|
||||
if success {
|
||||
note = "tx_status=SUCCESS"
|
||||
}
|
||||
return &pbredpacket.ParseTxEventsResp{Chain: "tron", TxHash: req.TxHash, Events: out, Note: note}, nil
|
||||
}
|
||||
|
||||
if s.chainClient != nil {
|
||||
txHashBytes := common.HexToHash(req.TxHash)
|
||||
events, err := s.chainClient.ParseTransactionReceipt(ctx, txHashBytes)
|
||||
success, events, err := s.chainClient.ParseTransactionReceiptWithStatus(ctx, txHashBytes)
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("parse tx receipt failed: " + err.Error())
|
||||
}
|
||||
@ -206,10 +210,15 @@ func (s *redPacketServer) ParseTxEvents(ctx context.Context, req *pbredpacket.Pa
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
note := "tx_status=FAILED"
|
||||
if success {
|
||||
note = "tx_status=SUCCESS"
|
||||
}
|
||||
return &pbredpacket.ParseTxEventsResp{
|
||||
Chain: "eth",
|
||||
TxHash: req.TxHash,
|
||||
Events: out,
|
||||
Note: note,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -113,12 +113,29 @@ func (c *ChainClient) SignClaim(digest [32]byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (c *ChainClient) ParseTransactionReceipt(ctx context.Context, txHash common.Hash) ([]*ParsedEvent, error) {
|
||||
_, events, err := c.ParseTransactionReceiptWithStatus(ctx, txHash)
|
||||
return events, err
|
||||
}
|
||||
|
||||
// ParseTransactionReceiptWithStatus fetches tx receipt once and returns both
|
||||
// execution status and decoded contract events.
|
||||
func (c *ChainClient) ParseTransactionReceiptWithStatus(ctx context.Context, txHash common.Hash) (bool, []*ParsedEvent, error) {
|
||||
receipt, err := c.client.TransactionReceipt(ctx, txHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get receipt failed: %w", err)
|
||||
return false, nil, fmt.Errorf("get receipt failed: %w", err)
|
||||
}
|
||||
events, err := ParseEventsFromLogs(receipt.Logs, c.contractABI)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return receipt.Status == types.ReceiptStatusSuccessful, events, nil
|
||||
}
|
||||
|
||||
return ParseEventsFromLogs(receipt.Logs, c.contractABI)
|
||||
// IsTransactionSuccessful reports whether the EVM transaction executed
|
||||
// successfully according to receipt.status (1=success, 0=failure).
|
||||
func (c *ChainClient) IsTransactionSuccessful(ctx context.Context, txHash common.Hash) (bool, error) {
|
||||
success, _, err := c.ParseTransactionReceiptWithStatus(ctx, txHash)
|
||||
return success, err
|
||||
}
|
||||
|
||||
func (c *ChainClient) ContractAddress() common.Address {
|
||||
|
||||
@ -63,17 +63,28 @@ func (t *TronClient) FullNodeURL() string {
|
||||
}
|
||||
|
||||
func (t *TronClient) ParseTransactionReceipt(ctx context.Context, txID string) ([]*ParsedEvent, error) {
|
||||
_, events, err := t.ParseTransactionReceiptWithStatus(ctx, txID)
|
||||
return events, err
|
||||
}
|
||||
|
||||
// ParseTransactionReceiptWithStatus fetches tx info once and returns both
|
||||
// execution status and decoded contract events.
|
||||
func (t *TronClient) ParseTransactionReceiptWithStatus(ctx context.Context, txID string) (bool, []*ParsedEvent, error) {
|
||||
info, err := t.getTransactionInfo(ctx, txID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
logs, err := tronLogsToEVMLogs(info, txID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
return ParseEventsFromLogs(logs, t.parsedABI)
|
||||
events, err := ParseEventsFromLogs(logs, t.parsedABI)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
success := strings.EqualFold(info.Receipt.Result, "SUCCESS")
|
||||
return success, events, nil
|
||||
}
|
||||
|
||||
func (t *TronClient) SendAdminTransaction(ctx context.Context, methodName string, args ...interface{}) (string, error) {
|
||||
@ -111,13 +122,23 @@ func (t *TronClient) GetSignMessageForTron(ctx context.Context, packetID *big.In
|
||||
type tronTxInfoResp struct {
|
||||
ID string `json:"id"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
Log []struct {
|
||||
Receipt struct {
|
||||
Result string `json:"result"`
|
||||
} `json:"receipt"`
|
||||
Log []struct {
|
||||
Address string `json:"address"`
|
||||
Topics []string `json:"topics"`
|
||||
Data string `json:"data"`
|
||||
} `json:"log"`
|
||||
}
|
||||
|
||||
// IsTransactionSuccessful reports whether the TRON transaction execution
|
||||
// succeeded based on receipt.result == "SUCCESS".
|
||||
func (t *TronClient) IsTransactionSuccessful(ctx context.Context, txID string) (bool, error) {
|
||||
success, _, err := t.ParseTransactionReceiptWithStatus(ctx, txID)
|
||||
return success, err
|
||||
}
|
||||
|
||||
func getParamTypes(args []interface{}) string {
|
||||
types := make([]string, len(args))
|
||||
for i, arg := range args {
|
||||
|
||||
@ -137,13 +137,17 @@ func Start(ctx context.Context, conf *Config, registry discovery.SvcDiscoveryReg
|
||||
|
||||
pbredpacket.RegisterRedPacketServer(server, srv)
|
||||
|
||||
if chainClient != nil {
|
||||
ethIndexer := chain.NewIndexer(chainClient, repo, conf.RpcConfig.Indexer.PollInterval, 0, conf.RpcConfig.Indexer.MaxBlocksPerPoll)
|
||||
ethIndexer.Start(ctx)
|
||||
}
|
||||
if tronClient != nil {
|
||||
tronIndexer := chain.NewTronIndexer(tronClient, repo, conf.RpcConfig.Indexer.PollInterval, 0)
|
||||
tronIndexer.Start(ctx)
|
||||
if conf.RpcConfig.Indexer.PollInterval > 0 {
|
||||
if chainClient != nil {
|
||||
ethIndexer := chain.NewIndexer(chainClient, repo, conf.RpcConfig.Indexer.PollInterval, 0)
|
||||
ethIndexer.Start(ctx)
|
||||
}
|
||||
if tronClient != nil {
|
||||
tronIndexer := chain.NewTronIndexer(tronClient, repo, conf.RpcConfig.Indexer.PollInterval, 0)
|
||||
tronIndexer.Start(ctx)
|
||||
}
|
||||
} else {
|
||||
log.ZInfo(ctx, "redpacket indexer disabled by config", "pollInterval", conf.RpcConfig.Indexer.PollInterval)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -124,6 +124,12 @@ func (s *redPacketServer) CreatedCallback(ctx context.Context, req *pbredpacket.
|
||||
PacketID: createdPacket.PacketID,
|
||||
ChainID: createdPacket.ChainID,
|
||||
ContractAddress: createdPacket.ContractAddress,
|
||||
CreatorWallet: createdPacket.CreatorWallet,
|
||||
PacketType: createdPacket.PacketType,
|
||||
Token: createdPacket.Token,
|
||||
TotalAmount: createdPacket.TotalAmount,
|
||||
TotalShares: createdPacket.TotalShares,
|
||||
ExpiryAt: createdPacket.ExpiryAt,
|
||||
TxHash: req.TxHash,
|
||||
GroupID: groupID,
|
||||
ScopeType: scopeType,
|
||||
@ -268,15 +274,36 @@ func (s *redPacketServer) ClaimResult(ctx context.Context, req *pbredpacket.Clai
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claimedEvent, err := s.resolveClaimedEvent(ctx, rp, req.TxHash)
|
||||
txSuccess, events, err := s.parseChainReceiptWithStatus(ctx, rp, req.TxHash)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "parse claim receipt failed", err, "txHash", req.TxHash)
|
||||
return &pbredpacket.ClaimResultResp{}, nil
|
||||
}
|
||||
if !txSuccess {
|
||||
if markErr := s.markClaimFailed(ctx, req.PacketID, currentUserID, req.Claimer, req.TxHash); markErr != nil {
|
||||
log.ZWarn(ctx, "mark claim failed status failed", markErr, "txHash", req.TxHash)
|
||||
}
|
||||
return &pbredpacket.ClaimResultResp{}, nil
|
||||
}
|
||||
|
||||
claimedEvent, err := resolveClaimedEventFromParsedEvents(rp, events)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "resolve claim event failed", err, "txHash", req.TxHash)
|
||||
if markErr := s.markClaimFailed(ctx, req.PacketID, currentUserID, req.Claimer, req.TxHash); markErr != nil {
|
||||
log.ZWarn(ctx, "mark claim failed status failed", markErr, "txHash", req.TxHash)
|
||||
}
|
||||
return &pbredpacket.ClaimResultResp{}, nil
|
||||
}
|
||||
if claimedEvent == nil {
|
||||
if markErr := s.markClaimFailed(ctx, req.PacketID, currentUserID, req.Claimer, req.TxHash); markErr != nil {
|
||||
log.ZWarn(ctx, "mark claim failed status failed", markErr, "txHash", req.TxHash)
|
||||
}
|
||||
return &pbredpacket.ClaimResultResp{}, nil
|
||||
}
|
||||
if !strings.EqualFold(claimedEvent.ClaimerWallet, req.Claimer) {
|
||||
if markErr := s.markClaimFailed(ctx, req.PacketID, currentUserID, req.Claimer, req.TxHash); markErr != nil {
|
||||
log.ZWarn(ctx, "mark claim failed status failed", markErr, "txHash", req.TxHash)
|
||||
}
|
||||
return nil, errs.ErrArgs.WrapMsg(fmt.Sprintf("claim event claimer mismatch: got %s want %s", claimedEvent.ClaimerWallet, req.Claimer))
|
||||
}
|
||||
|
||||
@ -311,6 +338,34 @@ func (s *redPacketServer) ClaimResult(ctx context.Context, req *pbredpacket.Clai
|
||||
return &pbredpacket.ClaimResultResp{}, nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) parseChainReceiptWithStatus(ctx context.Context, rp *model.RedPacket, txHash string) (bool, []*chain.ParsedEvent, error) {
|
||||
switch rp.ChainType {
|
||||
case "EVM":
|
||||
if s.chainClient == nil {
|
||||
return false, nil, errs.ErrInternalServer.WrapMsg("evm client is unavailable")
|
||||
}
|
||||
return s.chainClient.ParseTransactionReceiptWithStatus(ctx, common.HexToHash(txHash))
|
||||
case "TRON":
|
||||
if s.tronClient == nil {
|
||||
return false, nil, errs.ErrInternalServer.WrapMsg("tron client is unavailable")
|
||||
}
|
||||
return s.tronClient.ParseTransactionReceiptWithStatus(ctx, txHash)
|
||||
default:
|
||||
return false, nil, errs.ErrArgs.WrapMsg("unsupported chain_type: " + rp.ChainType)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *redPacketServer) markClaimFailed(ctx context.Context, packetID, userID, claimer, txHash string) error {
|
||||
return s.db.SaveClaim(ctx, &model.RedPacketClaim{
|
||||
PacketID: packetID,
|
||||
UserID: userID,
|
||||
ClaimerWallet: claimer,
|
||||
ClaimTxHash: txHash,
|
||||
Status: "FAILED",
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// canClaim runs the claim-eligibility check (formerly RedPacketService.CanClaim).
|
||||
func (s *redPacketServer) canClaim(ctx context.Context, packetID, claimer, userID string) error {
|
||||
rp, err := s.db.GetRedPacketByPacketID(ctx, packetID)
|
||||
@ -344,6 +399,12 @@ type claimedEventSnapshot struct {
|
||||
BlockNumber uint64
|
||||
}
|
||||
|
||||
type refundedEventSnapshot struct {
|
||||
RefundTo string
|
||||
Amount string
|
||||
BlockNumber uint64
|
||||
}
|
||||
|
||||
type createdPacketSnapshot struct {
|
||||
PacketID string
|
||||
ChainID int64
|
||||
@ -367,10 +428,13 @@ func (s *redPacketServer) resolveCreatedPacket(ctx context.Context, rp *model.Re
|
||||
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
|
||||
}
|
||||
|
||||
events, err := s.chainClient.ParseTransactionReceipt(ctx, common.HexToHash(txHashHex))
|
||||
success, events, err := s.chainClient.ParseTransactionReceiptWithStatus(ctx, common.HexToHash(txHashHex))
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("parse created tx failed: " + err.Error())
|
||||
}
|
||||
if !success {
|
||||
return nil, errs.ErrArgs.WrapMsg("created tx execution failed on chain")
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if event.Name != "PacketCreated" {
|
||||
@ -396,10 +460,13 @@ func (s *redPacketServer) resolveCreatedPacket(ctx context.Context, rp *model.Re
|
||||
return buildFallbackCreatedPacket(rp, fallbackPacketID), nil
|
||||
}
|
||||
|
||||
events, err := s.tronClient.ParseTransactionReceipt(ctx, txHashHex)
|
||||
success, events, err := s.tronClient.ParseTransactionReceiptWithStatus(ctx, txHashHex)
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("parse tron created tx failed: " + err.Error())
|
||||
}
|
||||
if !success {
|
||||
return nil, errs.ErrArgs.WrapMsg("created tx execution failed on chain")
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if event.Name != "PacketCreated" {
|
||||
@ -795,7 +862,10 @@ func (s *redPacketServer) resolveClaimedEvent(ctx context.Context, rp *model.Red
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resolveClaimedEventFromParsedEvents(rp, events)
|
||||
}
|
||||
|
||||
func resolveClaimedEventFromParsedEvents(rp *model.RedPacket, events []*chain.ParsedEvent) (*claimedEventSnapshot, error) {
|
||||
for _, event := range events {
|
||||
if event.Name != "PacketClaimed" {
|
||||
continue
|
||||
@ -816,6 +886,24 @@ func (s *redPacketServer) resolveClaimedEvent(ctx context.Context, rp *model.Red
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func resolveRefundedEventFromParsedEvents(rp *model.RedPacket, events []*chain.ParsedEvent) (*refundedEventSnapshot, error) {
|
||||
for _, event := range events {
|
||||
if event.Name != "PacketRefunded" {
|
||||
continue
|
||||
}
|
||||
packetID := chain.GetPacketIDFromEvent(event).String()
|
||||
if packetID != rp.PacketID {
|
||||
return nil, errs.ErrArgs.WrapMsg(fmt.Sprintf("refund event packet mismatch: got %s want %s", packetID, rp.PacketID))
|
||||
}
|
||||
return &refundedEventSnapshot{
|
||||
RefundTo: strings.ToLower(chain.GetAddressFromEvent(event, "refundTo").Hex()),
|
||||
Amount: chain.GetAmountFromEvent(event).String(),
|
||||
BlockNumber: event.BlockNumber,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// maxTotalShares caps the number of shares to prevent abuse.
|
||||
const maxTotalShares = 10_000
|
||||
|
||||
@ -946,7 +1034,37 @@ func (s *redPacketServer) RequestRefund(ctx context.Context, req *pbredpacket.Re
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "redpacket refund submitted", "packetID", rp.PacketID, "txHash", txHash)
|
||||
return &pbredpacket.RequestRefundResp{TxHash: txHash, Status: "PENDING"}, nil
|
||||
txSuccess, events, parseErr := s.parseChainReceiptWithStatus(ctx, rp, txHash)
|
||||
if parseErr != nil {
|
||||
log.ZWarn(ctx, "parse refund receipt failed, fallback to async indexer", parseErr, "packetID", rp.PacketID, "txHash", txHash)
|
||||
return &pbredpacket.RequestRefundResp{TxHash: txHash, Status: "PENDING"}, nil
|
||||
}
|
||||
if !txSuccess {
|
||||
return &pbredpacket.RequestRefundResp{TxHash: txHash, Status: "FAILED"}, nil
|
||||
}
|
||||
|
||||
refundedEvent, err := resolveRefundedEventFromParsedEvents(rp, events)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "resolve refunded event failed, fallback to async indexer", err, "packetID", rp.PacketID, "txHash", txHash)
|
||||
return &pbredpacket.RequestRefundResp{TxHash: txHash, Status: "PENDING"}, nil
|
||||
}
|
||||
if refundedEvent == nil {
|
||||
return &pbredpacket.RequestRefundResp{TxHash: txHash, Status: "PENDING"}, nil
|
||||
}
|
||||
|
||||
if err := s.db.SaveRefund(ctx, &model.RedPacketRefund{
|
||||
PacketID: rp.PacketID,
|
||||
RefundTo: refundedEvent.RefundTo,
|
||||
TxHash: txHash,
|
||||
Amount: refundedEvent.Amount,
|
||||
CreatedAt: time.Now(),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.UpdateRedPacketStatus(ctx, rp.PacketID, "REFUNDED"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbredpacket.RequestRefundResp{TxHash: txHash, Status: "REFUNDED"}, nil
|
||||
}
|
||||
|
||||
func (s *redPacketServer) GetRefund(ctx context.Context, req *pbredpacket.GetRefundReq) (*pbredpacket.GetRefundResp, error) {
|
||||
|
||||
@ -75,6 +75,12 @@ func (m *RedPacketMgo) UpdateCreated(ctx context.Context, rp *model.RedPacket) e
|
||||
"tx_hash": rp.TxHash,
|
||||
"chain_id": rp.ChainID,
|
||||
"contract_address": rp.ContractAddress,
|
||||
"creator_wallet": rp.CreatorWallet,
|
||||
"packet_type": rp.PacketType,
|
||||
"token": rp.Token,
|
||||
"total_amount": rp.TotalAmount,
|
||||
"total_shares": rp.TotalShares,
|
||||
"expiry_at": rp.ExpiryAt,
|
||||
"group_id": rp.GroupID,
|
||||
"scope_type": rp.ScopeType,
|
||||
"receiver_user_id": rp.ReceiverUserID,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user