feat: 보상 재시도 + TX 확정 대기 + 에러 포맷 통일 + 품질 고도화
- 보상 지급 실패 시 즉시 재시도(3회 backoff) + DB 기록 + 백그라운드 워커 재시도 - WaitForTx 폴링으로 블록체인 TX 확정 대기, SendTxAndWait 편의 메서드 - chain 트랜잭션 코드 중복 제거 (userTx/operatorTx 헬퍼, 50% 감소) - AppError 기반 에러 응답 포맷 통일 (8개 코드, 전 핸들러 마이그레이션) - TX 에러 분류 + 한국어 사용자 메시지 매핑 (11가지 패턴) - player 서비스 테스트 20개 + chain WaitForTx 테스트 10개 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,3 +147,80 @@ func (c *Client) SendTx(tx any) (*SendTxResult, error) {
|
||||
err := c.Call("sendTx", tx, &result)
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// TxStatusResult mirrors the indexer.TxResult from the TOL Chain node.
|
||||
type TxStatusResult struct {
|
||||
TxID string `json:"tx_id"`
|
||||
BlockHeight int64 `json:"block_height"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// GetTxStatus queries the execution result of a transaction.
|
||||
// Returns nil result (no error) if the transaction has not been included in a block yet.
|
||||
func (c *Client) GetTxStatus(txID string) (*TxStatusResult, error) {
|
||||
var result *TxStatusResult
|
||||
err := c.Call("getTxStatus", map[string]string{"tx_id": txID}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TxError is returned when a transaction was included in a block but execution failed.
|
||||
type TxError struct {
|
||||
TxID string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *TxError) Error() string {
|
||||
return fmt.Sprintf("transaction %s failed: %s", e.TxID, e.Message)
|
||||
}
|
||||
|
||||
// DefaultTxTimeout is the default timeout for WaitForTx. PoA block intervals
|
||||
// are typically a few seconds, so 15s provides ample margin.
|
||||
const DefaultTxTimeout = 15 * time.Second
|
||||
|
||||
// SendTxAndWait sends a transaction and waits for block confirmation.
|
||||
// It combines SendTx + WaitForTx for the common fire-and-confirm pattern.
|
||||
func (c *Client) SendTxAndWait(tx any, timeout time.Duration) (*TxStatusResult, error) {
|
||||
sendResult, err := c.SendTx(tx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send tx: %w", err)
|
||||
}
|
||||
return c.WaitForTx(sendResult.TxID, timeout)
|
||||
}
|
||||
|
||||
// WaitForTx polls getTxStatus until the transaction is included in a block or
|
||||
// the timeout is reached. It returns the confirmed TxStatusResult on success,
|
||||
// a TxError if the transaction executed but failed, or a timeout error.
|
||||
func (c *Client) WaitForTx(txID string, timeout time.Duration) (*TxStatusResult, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
interval := 200 * time.Millisecond
|
||||
|
||||
for {
|
||||
result, err := c.GetTxStatus(txID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getTxStatus: %w", err)
|
||||
}
|
||||
if result != nil {
|
||||
if !result.Success {
|
||||
return result, &TxError{TxID: txID, Message: result.Error}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if time.Now().After(deadline) {
|
||||
return nil, fmt.Errorf("transaction %s not confirmed within %s", txID, timeout)
|
||||
}
|
||||
|
||||
time.Sleep(interval)
|
||||
// Increase interval up to 1s to reduce polling pressure.
|
||||
if interval < time.Second {
|
||||
interval = interval * 3 / 2
|
||||
if interval > time.Second {
|
||||
interval = time.Second
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user