Skip to content

Commit bc9e57e

Browse files
authored
Merge pull request #14 from optimism-java/add-api-flag
Add record move action from front
2 parents 1b28b6c + 79ac94d commit bc9e57e

13 files changed

Lines changed: 597 additions & 37 deletions

File tree

.env

Lines changed: 0 additions & 11 deletions
This file was deleted.

.github/workflows/docker-build.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,8 @@ on:
44
push:
55
branches:
66
- "main"
7-
- "dev"
87
tags:
98
- "v*.*.*"
10-
pull_request:
11-
branches:
12-
- "main"
13-
- "dev"
149

1510
jobs:
1611
docker:

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ mysql1
2727
meili_data
2828
postgres
2929
mysql
30-
MIGRATION_STATUS.md
31-
RPC_MANAGER_GUIDE.md
30+
.env
31+
FRONTEND_MOVE_API.md
3232

deploy/config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ sync:
3838
on_chain_status:
3939
claim_data_len:
4040
get_len_status:
41+
has_frontend_move:
4142
- table: game_claim_data
4243
index: gameclaims
4344
pk: id
@@ -54,6 +55,7 @@ sync:
5455
position:
5556
clock:
5657
output_block:
58+
is_from_frontend:
5759
- table: game_credit
5860
index: gamecredits
5961
pk: id

internal/api/frontend_move_api.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
"strconv"
6+
7+
"github.com/gin-gonic/gin"
8+
"github.com/optimism-java/dispute-explorer/internal/handler"
9+
"github.com/optimism-java/dispute-explorer/internal/schema"
10+
"github.com/optimism-java/dispute-explorer/internal/svc"
11+
"github.com/optimism-java/dispute-explorer/pkg/log"
12+
"gorm.io/gorm"
13+
)
14+
15+
type FrontendMoveAPI struct {
16+
handler *handler.FrontendMoveHandler
17+
}
18+
19+
// NewFrontendMoveAPI creates a new FrontendMoveAPI
20+
func NewFrontendMoveAPI(svc *svc.ServiceContext) *FrontendMoveAPI {
21+
return &FrontendMoveAPI{
22+
handler: handler.NewFrontendMoveHandler(svc),
23+
}
24+
}
25+
26+
// RecordMoveResponse response for recording Move transactions
27+
type RecordMoveResponse struct {
28+
Success bool `json:"success"`
29+
Message string `json:"message"`
30+
}
31+
32+
// FrontendMovesResponse response for frontend-initiated Move transaction list
33+
type FrontendMovesResponse struct {
34+
Success bool `json:"success"`
35+
Data []schema.FrontendMoveTransaction `json:"data"`
36+
Total int64 `json:"total"`
37+
Page int `json:"page"`
38+
Size int `json:"size"`
39+
}
40+
41+
// FrontendMoveDetailResponse response for frontend-initiated Move transaction details
42+
type FrontendMoveDetailResponse struct {
43+
Success bool `json:"success"`
44+
Data *schema.FrontendMoveTransaction `json:"data,omitempty"`
45+
Message string `json:"message,omitempty"`
46+
}
47+
48+
// @Summary Record frontend move transaction
49+
// @schemes
50+
// @Description Record a move transaction initiated from frontend
51+
// @Accept json
52+
// @Produce json
53+
// @Param request body handler.FrontendMoveRequest true "Frontend move request"
54+
// @Success 200 {object} RecordMoveResponse
55+
// @Router /disputegames/frontend-move [post]
56+
func (api *FrontendMoveAPI) RecordMove(c *gin.Context) {
57+
var req handler.FrontendMoveRequest
58+
if err := c.ShouldBindJSON(&req); err != nil {
59+
log.Errorf("[FrontendMoveAPI] Invalid request: %v", err)
60+
c.JSON(http.StatusBadRequest, RecordMoveResponse{
61+
Success: false,
62+
Message: "Invalid request format: " + err.Error(),
63+
})
64+
return
65+
}
66+
67+
err := api.handler.RecordFrontendMove(&req)
68+
if err != nil {
69+
log.Errorf("[FrontendMoveAPI] Failed to record frontend move: %v", err)
70+
c.JSON(http.StatusInternalServerError, RecordMoveResponse{
71+
Success: false,
72+
Message: "Failed to record move transaction: " + err.Error(),
73+
})
74+
return
75+
}
76+
77+
c.JSON(http.StatusOK, RecordMoveResponse{
78+
Success: true,
79+
Message: "Move transaction recorded successfully",
80+
})
81+
}
82+
83+
// @Summary Get frontend moves by game
84+
// @schemes
85+
// @Description Get all frontend move transactions for a specific dispute game
86+
// @Accept json
87+
// @Produce json
88+
// @Param address path string true "Dispute game contract address"
89+
// @Param page query int false "Page number (default: 1)"
90+
// @Param size query int false "Page size (default: 10)"
91+
// @Success 200 {object} FrontendMovesResponse
92+
// @Router /disputegames/:address/frontend-moves [get]
93+
func (api *FrontendMoveAPI) GetMovesByGame(c *gin.Context) {
94+
gameContract := c.Param("address")
95+
if gameContract == "" {
96+
c.JSON(http.StatusBadRequest, FrontendMovesResponse{
97+
Success: false,
98+
})
99+
return
100+
}
101+
102+
// Parse pagination parameters
103+
page := 1
104+
size := 10
105+
if pageStr := c.Query("page"); pageStr != "" {
106+
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
107+
page = p
108+
}
109+
}
110+
if sizeStr := c.Query("size"); sizeStr != "" {
111+
if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 && s <= 100 {
112+
size = s
113+
}
114+
}
115+
116+
moves, total, err := api.handler.GetFrontendMovesByGame(gameContract, page, size)
117+
if err != nil {
118+
log.Errorf("[FrontendMoveAPI] Failed to get frontend moves: %v", err)
119+
c.JSON(http.StatusInternalServerError, FrontendMovesResponse{
120+
Success: false,
121+
})
122+
return
123+
}
124+
125+
c.JSON(http.StatusOK, FrontendMovesResponse{
126+
Success: true,
127+
Data: moves,
128+
Total: total,
129+
Page: page,
130+
Size: size,
131+
})
132+
}
133+
134+
// @Summary Get frontend move by transaction hash
135+
// @schemes
136+
// @Description Get frontend move transaction details by transaction hash
137+
// @Accept json
138+
// @Produce json
139+
// @Param txhash path string true "Transaction hash"
140+
// @Success 200 {object} FrontendMoveDetailResponse
141+
// @Router /disputegames/frontend-move/:txhash [get]
142+
func (api *FrontendMoveAPI) GetMoveByTxHash(c *gin.Context) {
143+
txHash := c.Param("txhash")
144+
if txHash == "" {
145+
c.JSON(http.StatusBadRequest, FrontendMoveDetailResponse{
146+
Success: false,
147+
Message: "Transaction hash is required",
148+
})
149+
return
150+
}
151+
152+
move, err := api.handler.GetFrontendMoveByTxHash(txHash)
153+
if err != nil {
154+
log.Errorf("[FrontendMoveAPI] Failed to get frontend move: %v", err)
155+
statusCode := http.StatusInternalServerError
156+
if err == gorm.ErrRecordNotFound {
157+
statusCode = http.StatusNotFound
158+
}
159+
c.JSON(statusCode, FrontendMoveDetailResponse{
160+
Success: false,
161+
Message: err.Error(),
162+
})
163+
return
164+
}
165+
166+
c.JSON(http.StatusOK, FrontendMoveDetailResponse{
167+
Success: true,
168+
Data: move,
169+
})
170+
}
171+
172+
// @Summary Get dispute games with frontend move flag
173+
// @schemes
174+
// @Description Get all dispute games with information about whether they contain frontend-initiated moves
175+
// @Accept json
176+
// @Produce json
177+
// @Param page query int false "Page number (default: 1)"
178+
// @Param size query int false "Page size (default: 10)"
179+
// @Param frontend_only query bool false "Only show games with frontend moves"
180+
// @Success 200
181+
// @Router /disputegames/with-frontend-flag [get]
182+
func (api *FrontendMoveAPI) GetGamesWithFrontendFlag(c *gin.Context) {
183+
// Parse pagination parameters
184+
page := 1
185+
size := 10
186+
if pageStr := c.Query("page"); pageStr != "" {
187+
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
188+
page = p
189+
}
190+
}
191+
if sizeStr := c.Query("size"); sizeStr != "" {
192+
if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 && s <= 100 {
193+
size = s
194+
}
195+
}
196+
197+
frontendOnly := c.Query("frontend_only") == "true"
198+
199+
// Build query
200+
query := api.handler.GetServiceContext().DB.Model(&schema.DisputeGame{}).
201+
Select("dispute_games.*, COALESCE(frontend_stats.has_frontend_move, false) as has_frontend_move").
202+
Joins(`LEFT JOIN (
203+
SELECT game_contract, true as has_frontend_move
204+
FROM frontend_move_transactions
205+
WHERE status = ?
206+
GROUP BY game_contract
207+
) frontend_stats ON dispute_games.game_contract = frontend_stats.game_contract`, schema.FrontendMoveStatusConfirmed)
208+
209+
if frontendOnly {
210+
query = query.Where("frontend_stats.has_frontend_move = true")
211+
}
212+
213+
var total int64
214+
err := query.Count(&total).Error
215+
if err != nil {
216+
log.Errorf("[FrontendMoveAPI] Failed to count games: %v", err)
217+
c.JSON(http.StatusInternalServerError, gin.H{
218+
"success": false,
219+
"message": "Failed to count games",
220+
})
221+
return
222+
}
223+
224+
var games []map[string]interface{}
225+
offset := (page - 1) * size
226+
err = query.Offset(offset).Limit(size).Order("created_at DESC").Find(&games).Error
227+
if err != nil {
228+
log.Errorf("[FrontendMoveAPI] Failed to get games: %v", err)
229+
c.JSON(http.StatusInternalServerError, gin.H{
230+
"success": false,
231+
"message": "Failed to get games",
232+
})
233+
return
234+
}
235+
236+
c.JSON(http.StatusOK, gin.H{
237+
"success": true,
238+
"data": games,
239+
"total": total,
240+
"page": page,
241+
"size": size,
242+
})
243+
}

0 commit comments

Comments
 (0)