更新 红包逻辑 去掉扫块

pull/3727/head
panda 2 weeks ago
parent dba858f4fa
commit 1b4aba92ff

@ -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. 前端推荐调用顺序
### 6.1 创建红包
1. 用户登录业务系统
2. 前端请求 `/api/redpacket/create-order`
3. 拿到 `biz_id` 后,钱包调用链上创建红包方法
4. 从交易回执/事件拿到 `tx_hash`、`packet_id`
5. 前端请求 `/api/redpacket/created-callback`
6. 前端请求 `/api/redpacket/detail` 刷新状态(确认 `ACTIVE`
### 5.1 首次使用钱包领取
### 6.2 首次使用钱包领取
1. 用户登录业务系统
2. 前端请求 `/wallet-bind/challenge`
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,5 +27,6 @@ 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

@ -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)
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…
Cancel
Save