diff --git a/.env b/.env deleted file mode 100644 index f71361e..0000000 --- a/.env +++ /dev/null @@ -1,11 +0,0 @@ -LOG_FORMAT=console -MYSQL_DATA_SOURCE=root:root@tcp(mysql:3306)/dispute_explorer?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true -BLOCKCHAIN=seplia -L1_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/lV2e-64nNnEMUA7UG0IT0uwjzlxEI512 -L2_RPC_URL=https://opt-sepolia.g.alchemy.com/v2/FPgbOkDCgG8t0ppZ6TwZXLucr1wl_us4 -RPC_RATE_LIMIT=15 -RPC_RATE_BURST=5 -FROM_BLOCK_NUMBER=5515562 -FROM_BLOCK_HASH=0x5205c17557759edaef9120f56af802aeaa2827a60d674a0413e77e9c515bdfba -DISPUTE_GAME_PROXY_CONTRACT=0x05F9613aDB30026FFd634f38e5C4dFd30a197Fa1 -API_PORT=8080 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 671853e..ed9abbc 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -4,13 +4,8 @@ on: push: branches: - "main" - - "dev" tags: - "v*.*.*" - pull_request: - branches: - - "main" - - "dev" jobs: docker: diff --git a/.gitignore b/.gitignore index f943960..c9176e4 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,6 @@ mysql1 meili_data postgres mysql -MIGRATION_STATUS.md -RPC_MANAGER_GUIDE.md +.env +FRONTEND_MOVE_API.md diff --git a/deploy/config.yml b/deploy/config.yml index 472a38a..65d518d 100644 --- a/deploy/config.yml +++ b/deploy/config.yml @@ -38,6 +38,7 @@ sync: on_chain_status: claim_data_len: get_len_status: + has_frontend_move: - table: game_claim_data index: gameclaims pk: id @@ -54,6 +55,7 @@ sync: position: clock: output_block: + is_from_frontend: - table: game_credit index: gamecredits pk: id diff --git a/internal/api/frontend_move_api.go b/internal/api/frontend_move_api.go new file mode 100644 index 0000000..e7b5d0d --- /dev/null +++ b/internal/api/frontend_move_api.go @@ -0,0 +1,243 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/optimism-java/dispute-explorer/internal/handler" + "github.com/optimism-java/dispute-explorer/internal/schema" + "github.com/optimism-java/dispute-explorer/internal/svc" + "github.com/optimism-java/dispute-explorer/pkg/log" + "gorm.io/gorm" +) + +type FrontendMoveAPI struct { + handler *handler.FrontendMoveHandler +} + +// NewFrontendMoveAPI creates a new FrontendMoveAPI +func NewFrontendMoveAPI(svc *svc.ServiceContext) *FrontendMoveAPI { + return &FrontendMoveAPI{ + handler: handler.NewFrontendMoveHandler(svc), + } +} + +// RecordMoveResponse response for recording Move transactions +type RecordMoveResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// FrontendMovesResponse response for frontend-initiated Move transaction list +type FrontendMovesResponse struct { + Success bool `json:"success"` + Data []schema.FrontendMoveTransaction `json:"data"` + Total int64 `json:"total"` + Page int `json:"page"` + Size int `json:"size"` +} + +// FrontendMoveDetailResponse response for frontend-initiated Move transaction details +type FrontendMoveDetailResponse struct { + Success bool `json:"success"` + Data *schema.FrontendMoveTransaction `json:"data,omitempty"` + Message string `json:"message,omitempty"` +} + +// @Summary Record frontend move transaction +// @schemes +// @Description Record a move transaction initiated from frontend +// @Accept json +// @Produce json +// @Param request body handler.FrontendMoveRequest true "Frontend move request" +// @Success 200 {object} RecordMoveResponse +// @Router /disputegames/frontend-move [post] +func (api *FrontendMoveAPI) RecordMove(c *gin.Context) { + var req handler.FrontendMoveRequest + if err := c.ShouldBindJSON(&req); err != nil { + log.Errorf("[FrontendMoveAPI] Invalid request: %v", err) + c.JSON(http.StatusBadRequest, RecordMoveResponse{ + Success: false, + Message: "Invalid request format: " + err.Error(), + }) + return + } + + err := api.handler.RecordFrontendMove(&req) + if err != nil { + log.Errorf("[FrontendMoveAPI] Failed to record frontend move: %v", err) + c.JSON(http.StatusInternalServerError, RecordMoveResponse{ + Success: false, + Message: "Failed to record move transaction: " + err.Error(), + }) + return + } + + c.JSON(http.StatusOK, RecordMoveResponse{ + Success: true, + Message: "Move transaction recorded successfully", + }) +} + +// @Summary Get frontend moves by game +// @schemes +// @Description Get all frontend move transactions for a specific dispute game +// @Accept json +// @Produce json +// @Param address path string true "Dispute game contract address" +// @Param page query int false "Page number (default: 1)" +// @Param size query int false "Page size (default: 10)" +// @Success 200 {object} FrontendMovesResponse +// @Router /disputegames/:address/frontend-moves [get] +func (api *FrontendMoveAPI) GetMovesByGame(c *gin.Context) { + gameContract := c.Param("address") + if gameContract == "" { + c.JSON(http.StatusBadRequest, FrontendMovesResponse{ + Success: false, + }) + return + } + + // Parse pagination parameters + page := 1 + size := 10 + if pageStr := c.Query("page"); pageStr != "" { + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + } + if sizeStr := c.Query("size"); sizeStr != "" { + if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 && s <= 100 { + size = s + } + } + + moves, total, err := api.handler.GetFrontendMovesByGame(gameContract, page, size) + if err != nil { + log.Errorf("[FrontendMoveAPI] Failed to get frontend moves: %v", err) + c.JSON(http.StatusInternalServerError, FrontendMovesResponse{ + Success: false, + }) + return + } + + c.JSON(http.StatusOK, FrontendMovesResponse{ + Success: true, + Data: moves, + Total: total, + Page: page, + Size: size, + }) +} + +// @Summary Get frontend move by transaction hash +// @schemes +// @Description Get frontend move transaction details by transaction hash +// @Accept json +// @Produce json +// @Param txhash path string true "Transaction hash" +// @Success 200 {object} FrontendMoveDetailResponse +// @Router /disputegames/frontend-move/:txhash [get] +func (api *FrontendMoveAPI) GetMoveByTxHash(c *gin.Context) { + txHash := c.Param("txhash") + if txHash == "" { + c.JSON(http.StatusBadRequest, FrontendMoveDetailResponse{ + Success: false, + Message: "Transaction hash is required", + }) + return + } + + move, err := api.handler.GetFrontendMoveByTxHash(txHash) + if err != nil { + log.Errorf("[FrontendMoveAPI] Failed to get frontend move: %v", err) + statusCode := http.StatusInternalServerError + if err == gorm.ErrRecordNotFound { + statusCode = http.StatusNotFound + } + c.JSON(statusCode, FrontendMoveDetailResponse{ + Success: false, + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, FrontendMoveDetailResponse{ + Success: true, + Data: move, + }) +} + +// @Summary Get dispute games with frontend move flag +// @schemes +// @Description Get all dispute games with information about whether they contain frontend-initiated moves +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Param size query int false "Page size (default: 10)" +// @Param frontend_only query bool false "Only show games with frontend moves" +// @Success 200 +// @Router /disputegames/with-frontend-flag [get] +func (api *FrontendMoveAPI) GetGamesWithFrontendFlag(c *gin.Context) { + // Parse pagination parameters + page := 1 + size := 10 + if pageStr := c.Query("page"); pageStr != "" { + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + } + if sizeStr := c.Query("size"); sizeStr != "" { + if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 && s <= 100 { + size = s + } + } + + frontendOnly := c.Query("frontend_only") == "true" + + // Build query + query := api.handler.GetServiceContext().DB.Model(&schema.DisputeGame{}). + Select("dispute_games.*, COALESCE(frontend_stats.has_frontend_move, false) as has_frontend_move"). + Joins(`LEFT JOIN ( + SELECT game_contract, true as has_frontend_move + FROM frontend_move_transactions + WHERE status = ? + GROUP BY game_contract + ) frontend_stats ON dispute_games.game_contract = frontend_stats.game_contract`, schema.FrontendMoveStatusConfirmed) + + if frontendOnly { + query = query.Where("frontend_stats.has_frontend_move = true") + } + + var total int64 + err := query.Count(&total).Error + if err != nil { + log.Errorf("[FrontendMoveAPI] Failed to count games: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to count games", + }) + return + } + + var games []map[string]interface{} + offset := (page - 1) * size + err = query.Offset(offset).Limit(size).Order("created_at DESC").Find(&games).Error + if err != nil { + log.Errorf("[FrontendMoveAPI] Failed to get games: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "Failed to get games", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": games, + "total": total, + "page": page, + "size": size, + }) +} diff --git a/internal/handler/frontend_move.go b/internal/handler/frontend_move.go new file mode 100644 index 0000000..7b96814 --- /dev/null +++ b/internal/handler/frontend_move.go @@ -0,0 +1,229 @@ +package handler + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/optimism-java/dispute-explorer/internal/schema" + "github.com/optimism-java/dispute-explorer/internal/svc" + "github.com/optimism-java/dispute-explorer/pkg/log" + + "gorm.io/gorm" +) + +// FrontendMoveRequest request structure for frontend-initiated move transactions +type FrontendMoveRequest struct { + GameContract string `json:"game_contract" binding:"required"` // Dispute Game contract address + TxHash string `json:"tx_hash" binding:"required"` // Transaction hash + Claimant string `json:"claimant" binding:"required"` // Initiator address + ParentIndex string `json:"parent_index" binding:"required"` // Parent index + Claim string `json:"claim" binding:"required"` // Claim data + IsAttack bool `json:"is_attack"` // Whether it's an attack + ChallengeIndex string `json:"challenge_index" binding:"required"` // Challenge index + DisputedClaim string `json:"disputed_claim" binding:"required"` // Disputed claim +} + +// FrontendMoveHandler handles frontend-initiated move transactions +type FrontendMoveHandler struct { + svc *svc.ServiceContext +} + +// NewFrontendMoveHandler creates a new FrontendMoveHandler +func NewFrontendMoveHandler(svc *svc.ServiceContext) *FrontendMoveHandler { + return &FrontendMoveHandler{ + svc: svc, + } +} + +// GetServiceContext gets ServiceContext +func (h *FrontendMoveHandler) GetServiceContext() *svc.ServiceContext { + return h.svc +} + +// RecordFrontendMove records frontend-initiated move transactions +func (h *FrontendMoveHandler) RecordFrontendMove(req *FrontendMoveRequest) error { + // Validate contract address format + if !common.IsHexAddress(req.GameContract) { + return fmt.Errorf("invalid game contract address: %s", req.GameContract) + } + + // Validate transaction hash format + if len(req.TxHash) != 66 || !strings.HasPrefix(req.TxHash, "0x") { + return fmt.Errorf("invalid transaction hash format: %s", req.TxHash) + } + + // Validate initiator address format + if !common.IsHexAddress(req.Claimant) { + return fmt.Errorf("invalid claimant address: %s", req.Claimant) + } + + // Check if this transaction has already been recorded + var existingRecord schema.FrontendMoveTransaction + err := h.svc.DB.Where("tx_hash = ?", req.TxHash).First(&existingRecord).Error + if err == nil { + log.Warnf("[FrontendMoveHandler] Transaction %s already recorded", req.TxHash) + return fmt.Errorf("transaction %s already recorded", req.TxHash) + } else if err != gorm.ErrRecordNotFound { + return fmt.Errorf("failed to check existing record: %v", err) + } + + // Create new record + frontendMove := &schema.FrontendMoveTransaction{ + GameContract: req.GameContract, + TxHash: req.TxHash, + Claimant: req.Claimant, + ParentIndex: req.ParentIndex, + Claim: req.Claim, + IsAttack: req.IsAttack, + ChallengeIndex: req.ChallengeIndex, + DisputedClaim: req.DisputedClaim, + Status: schema.FrontendMoveStatusPending, + SubmittedAt: time.Now().Unix(), + } + + // Save to database + err = h.svc.DB.Create(frontendMove).Error + if err != nil { + return fmt.Errorf("failed to save frontend move record: %v", err) + } + + log.Infof("[FrontendMoveHandler] Recorded frontend move transaction: %s for game: %s", req.TxHash, req.GameContract) + + // Asynchronously check transaction status + go h.monitorTransactionStatus(frontendMove.ID, req.TxHash) + + return nil +} + +// monitorTransactionStatus monitors transaction status +func (h *FrontendMoveHandler) monitorTransactionStatus(recordID int64, txHash string) { + maxRetries := 60 // Maximum 60 retries, 10 seconds interval each + retryInterval := 10 * time.Second + + for i := 0; i < maxRetries; i++ { + time.Sleep(retryInterval) + + // Query transaction status + receipt, err := h.svc.L1RPC.TransactionReceipt(context.Background(), common.HexToHash(txHash)) + if err != nil { + log.Debugf("[FrontendMoveHandler] Transaction %s not yet mined or error: %v", txHash, err) + continue + } + + // Update record status + var status string + var confirmedAt int64 + if receipt.Status == 1 { + status = schema.FrontendMoveStatusConfirmed + confirmedAt = time.Now().Unix() + } else { + status = schema.FrontendMoveStatusFailed + } + err = h.svc.DB.Model(&schema.FrontendMoveTransaction{}). + Where("id = ?", recordID). + Updates(map[string]interface{}{ + "status": status, + "block_number": receipt.BlockNumber.Int64(), + "confirmed_at": confirmedAt, + }).Error + if err != nil { + log.Errorf("[FrontendMoveHandler] Failed to update transaction status for %s: %v", txHash, err) + return + } + log.Infof("[FrontendMoveHandler] Transaction %s status updated to %s", txHash, status) + + // If transaction is successful, mark related records + if receipt.Status == 1 { + h.markRelatedRecords(txHash, receipt.BlockNumber.Int64()) + } + + return + } + + // If timeout without finding transaction, mark as failed + err := h.svc.DB.Model(&schema.FrontendMoveTransaction{}). + Where("id = ?", recordID). + Updates(map[string]interface{}{ + "status": schema.FrontendMoveStatusFailed, + "error_message": "Transaction timeout", + }).Error + if err != nil { + log.Errorf("[FrontendMoveHandler] Failed to update timeout status for %s: %v", txHash, err) + } + + log.Warnf("[FrontendMoveHandler] Transaction %s monitoring timeout", txHash) +} + +// markRelatedRecords marks related block and event records +func (h *FrontendMoveHandler) markRelatedRecords(txHash string, blockNumber int64) { + // Mark related blocks + err := h.svc.DB.Model(&schema.DisputeGame{}). + Where("block_number = ?", blockNumber). + Update("has_frontend_move", true).Error + if err != nil { + log.Errorf("[FrontendMoveHandler] Failed to mark block %d for tx %s: %v", blockNumber, txHash, err) + } + + // Mark related events + err = h.svc.DB.Model(&schema.GameClaimData{}). + Where("tx_hash = ?", txHash). + Update("is_from_frontend", true).Error + if err != nil { + log.Errorf("[FrontendMoveHandler] Failed to mark events for tx %s: %v", txHash, err) + } + + log.Infof("[FrontendMoveHandler] Marked related records for tx %s in block %d", txHash, blockNumber) +} + +// GetFrontendMovesByGame gets frontend-initiated move transactions for specified game +func (h *FrontendMoveHandler) GetFrontendMovesByGame(gameContract string, page, size int) ([]schema.FrontendMoveTransaction, int64, error) { + var moves []schema.FrontendMoveTransaction + var total int64 + + // Validate contract address format + if !common.IsHexAddress(gameContract) { + return nil, 0, fmt.Errorf("invalid game contract address: %s", gameContract) + } + + // Get total count + err := h.svc.DB.Model(&schema.FrontendMoveTransaction{}). + Where("game_contract = ?", gameContract). + Count(&total).Error + if err != nil { + return nil, 0, fmt.Errorf("failed to count frontend moves: %v", err) + } + + // Get paginated data + offset := (page - 1) * size + err = h.svc.DB.Where("game_contract = ?", gameContract). + Order("created_at DESC"). + Offset(offset). + Limit(size). + Find(&moves).Error + if err != nil { + return nil, 0, fmt.Errorf("failed to get frontend moves: %v", err) + } + + return moves, total, nil +} + +// GetFrontendMoveByTxHash gets frontend-initiated move transaction by transaction hash +func (h *FrontendMoveHandler) GetFrontendMoveByTxHash(txHash string) (*schema.FrontendMoveTransaction, error) { + if len(txHash) != 66 || !strings.HasPrefix(txHash, "0x") { + return nil, fmt.Errorf("invalid transaction hash format: %s", txHash) + } + + var move schema.FrontendMoveTransaction + err := h.svc.DB.Where("tx_hash = ?", txHash).First(&move).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("frontend move not found for tx: %s", txHash) + } + return nil, fmt.Errorf("failed to get frontend move: %v", err) + } + + return &move, nil +} diff --git a/internal/schema/dispute_game.go b/internal/schema/dispute_game.go index 164accc..d5cc970 100644 --- a/internal/schema/dispute_game.go +++ b/internal/schema/dispute_game.go @@ -33,6 +33,7 @@ type DisputeGame struct { OnChainStatus string `json:"on_chain_status"` ClaimDataLen int64 `json:"claim_data_len"` GetLenStatus bool `json:"get_len_status"` + HasFrontendMove bool `json:"has_frontend_move" gorm:"default:false"` // Whether contains frontend-initiated move transactions } func (DisputeGame) TableName() string { diff --git a/internal/schema/frontend_move_transaction.go b/internal/schema/frontend_move_transaction.go new file mode 100644 index 0000000..36a5d88 --- /dev/null +++ b/internal/schema/frontend_move_transaction.go @@ -0,0 +1,29 @@ +package schema + +const ( + FrontendMoveStatusPending = "pending" + FrontendMoveStatusConfirmed = "confirmed" + FrontendMoveStatusFailed = "failed" +) + +// FrontendMoveTransaction records move transaction information initiated from frontend +type FrontendMoveTransaction struct { + Base + GameContract string `json:"game_contract" gorm:"index:idx_game_contract"` // Dispute Game contract address + TxHash string `json:"tx_hash" gorm:"type:varchar(128);uniqueIndex:idx_tx_hash"` // Transaction hash + Claimant string `json:"claimant" gorm:"type:varchar(128)"` // Initiator address + ParentIndex string `json:"parent_index"` // Parent index + Claim string `json:"claim"` // Claim data + IsAttack bool `json:"is_attack"` // Whether it's an attack + ChallengeIndex string `json:"challenge_index"` // Challenge index + DisputedClaim string `json:"disputed_claim"` // Disputed claim + BlockNumber int64 `json:"block_number"` // Block number + Status string `json:"status" gorm:"type:varchar(20);default:pending"` // Status + ErrorMessage string `json:"error_message,omitempty"` // Error message (if any) + SubmittedAt int64 `json:"submitted_at"` // Submission timestamp + ConfirmedAt int64 `json:"confirmed_at,omitempty"` // Confirmation timestamp +} + +func (FrontendMoveTransaction) TableName() string { + return "frontend_move_transactions" +} diff --git a/internal/schema/game_claim_data.go b/internal/schema/game_claim_data.go index 13463b5..91e29bb 100644 --- a/internal/schema/game_claim_data.go +++ b/internal/schema/game_claim_data.go @@ -7,18 +7,19 @@ const ( type GameClaimData struct { Base - GameContract string `json:"game_contract"` - DataIndex int64 `json:"data_index"` - ParentIndex uint32 `json:"parent_index"` - CounteredBy string `json:"countered_by"` - Claimant string `json:"claimant"` - Bond string `json:"bond"` - Claim string `json:"claim"` - Position string `json:"position"` - Clock string `json:"clock"` - OutputBlock uint64 `json:"output_block"` - EventID int64 `json:"event_id"` - OnChainStatus string `json:"on_chain_status"` + GameContract string `json:"game_contract"` + DataIndex int64 `json:"data_index"` + ParentIndex uint32 `json:"parent_index"` + CounteredBy string `json:"countered_by"` + Claimant string `json:"claimant"` + Bond string `json:"bond"` + Claim string `json:"claim"` + Position string `json:"position"` + Clock string `json:"clock"` + OutputBlock uint64 `json:"output_block"` + EventID int64 `json:"event_id"` + OnChainStatus string `json:"on_chain_status"` + IsFromFrontend bool `json:"is_from_frontend" gorm:"default:false"` // Whether initiated from frontend } func (GameClaimData) TableName() string { diff --git a/main.go b/main.go index 98f7aee..f3fc9a0 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,9 @@ func main() { log.Info("listener running...\n") router := gin.Default() disputeGameHandler := api.NewDisputeGameHandler(sCtx.DB, sCtx.L1RPC, sCtx.L2RPC, cfg) + + // 新增:前端 Move 交易处理器 + frontendMoveAPI := api.NewFrontendMoveAPI(sCtx) docs.SwaggerInfo.Title = "Dispute Game Swagger API" docs.SwaggerInfo.Description = "This is a dispute-explorer server." docs.SwaggerInfo.BasePath = "/" @@ -43,6 +46,12 @@ func main() { router.POST("/disputegames/calculate/claim", disputeGameHandler.GetGamesClaimByPosition) router.GET("/disputegames/chainname", disputeGameHandler.GetCurrentBlockChain) + // 新增:前端 Move 交易相关路由 + router.POST("/disputegames/frontend-move", frontendMoveAPI.RecordMove) // 记录前端发起的 move 交易 + router.GET("/disputegames/:address/frontend-moves", frontendMoveAPI.GetMovesByGame) // 获取指定游戏的前端 move 交易 + router.GET("/disputegames/frontend-move/:txhash", frontendMoveAPI.GetMoveByTxHash) // 根据交易哈希获取前端 move 交易详情 + router.GET("/disputegames/with-frontend-flag", frontendMoveAPI.GetGamesWithFrontendFlag) // 获取带有前端发起标记的游戏列表 + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) err := router.Run(":" + cfg.APIPort) diff --git a/migration/version/migration_version.go b/migration/version/migration_version.go index 89ab971..c5023b6 100644 --- a/migration/version/migration_version.go +++ b/migration/version/migration_version.go @@ -9,6 +9,7 @@ import ( v5 "github.com/optimism-java/dispute-explorer/migration/version/v5" v6 "github.com/optimism-java/dispute-explorer/migration/version/v6" v7 "github.com/optimism-java/dispute-explorer/migration/version/v7" + v8 "github.com/optimism-java/dispute-explorer/migration/version/v8" ) var ModelSchemaList = []*gormigrate.Migration{ @@ -19,4 +20,5 @@ var ModelSchemaList = []*gormigrate.Migration{ &v5.AddOnChainStatusForDisputeGameTable, &v6.UpdateClaimDataClockColumnTable, &v7.AddClaimDataLenForDisputeGameTable, + &v8.AddFrontendMoveTrackingTable, } diff --git a/migration/version/v8/add_frontend_move_tracking.go b/migration/version/v8/add_frontend_move_tracking.go new file mode 100644 index 0000000..90019c5 --- /dev/null +++ b/migration/version/v8/add_frontend_move_tracking.go @@ -0,0 +1,64 @@ +package v8 + +import ( + "fmt" + + gormigrate "github.com/go-gormigrate/gormigrate/v2" + "github.com/optimism-java/dispute-explorer/internal/schema" + "gorm.io/gorm" +) + +var AddFrontendMoveTrackingTable = gormigrate.Migration{ + ID: "20240805_add_frontend_move_tracking", + Migrate: AddFrontendMoveTracking, +} + +func AddFrontendMoveTracking(db *gorm.DB) error { + // Create new frontend_move_transactions table + err := db.AutoMigrate(&schema.FrontendMoveTransaction{}) + if err != nil { + return fmt.Errorf("failed to create frontend_move_transactions table: %v", err) + } + + // Check and add has_frontend_move field to sync_blocks table + if !db.Migrator().HasColumn(&schema.DisputeGame{}, "has_frontend_move") { + err = db.Migrator().AddColumn(&schema.DisputeGame{}, "has_frontend_move") + if err != nil { + return fmt.Errorf("failed to add has_frontend_move column to dispute_game: %v", err) + } + + // Set default value for newly added field + err = db.Exec("UPDATE dispute_game SET has_frontend_move = FALSE WHERE has_frontend_move IS NULL").Error + if err != nil { + return fmt.Errorf("failed to set default value for has_frontend_move: %v", err) + } + + // Create index for new field + err = db.Exec("CREATE INDEX idx_dispute_game_has_frontend_move ON dispute_game(has_frontend_move)").Error + if err != nil { + return fmt.Errorf("failed to create index on has_frontend_move: %v", err) + } + } + + // Check and add is_from_frontend field to game_claim_data table + if !db.Migrator().HasColumn(&schema.GameClaimData{}, "is_from_frontend") { + err = db.Migrator().AddColumn(&schema.GameClaimData{}, "is_from_frontend") + if err != nil { + return fmt.Errorf("failed to add is_from_frontend column to game_claim_data: %v", err) + } + + // Set default value for newly added field + err = db.Exec("UPDATE game_claim_data SET is_from_frontend = FALSE WHERE is_from_frontend IS NULL").Error + if err != nil { + return fmt.Errorf("failed to set default value for is_from_frontend: %v", err) + } + + // Create index for new field + err = db.Exec("CREATE INDEX idx_game_claim_data_is_from_frontend ON game_claim_data(is_from_frontend)").Error + if err != nil { + return fmt.Errorf("failed to create index on is_from_frontend: %v", err) + } + } + + return nil +} diff --git a/pkg/log/log.go b/pkg/log/log.go index f617598..25b4510 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -104,9 +104,7 @@ func Init(level, format string) { std = New(options) } -// New create logger by opts which can custmoized by command arguments. -// -//nolint:revive +//nolint:revive // Intentionally returning unexported type for internal use func New(opts *Options) *zapLogger { if opts == nil { opts = NewOptions() @@ -294,16 +292,14 @@ func (l *zapLogger) Errorf(format string, v ...interface{}) { l.zapLogger.Sugar().Errorf(format, v...) } -//nolint:govet func ErrorR(format string, v ...interface{}) error { std.zapLogger.Sugar().Errorf(format, v...) - return fmt.Errorf(format, v) + return fmt.Errorf(format, v...) } -//nolint:govet func (l *zapLogger) ErrorR(format string, v ...interface{}) error { l.zapLogger.Sugar().Errorf(format, v...) - return fmt.Errorf(format, v) + return fmt.Errorf(format, v...) } // Errorw method output error level log.