diff --git a/relayer/alert/alert.go b/relayer/alert/alert.go index 4bfc414..e462887 100644 --- a/relayer/alert/alert.go +++ b/relayer/alert/alert.go @@ -11,6 +11,9 @@ const ( GetTunnelPacketErrorMsg = "Failed to get tunnel packet from BandChain" GetContractTunnelInfoErrorMsg = "Failed to get tunnel info from contract" PacketSigningStatusErrorMsg = "Failed tunnel packet signing status" + GetBlockErrorMsg = "Failed to get block from chain" + GetBalanceErrorMsg = "Failed to get balance from chain" + SaveDatabaseErrorMsg = "Failed to save to database" ) // Alert represents an object that triggers and resets alerts. diff --git a/relayer/chains/evm/provider.go b/relayer/chains/evm/provider.go index d902289..8466c93 100644 --- a/relayer/chains/evm/provider.go +++ b/relayer/chains/evm/provider.go @@ -171,7 +171,10 @@ func (cp *EVMChainProvider) RelayPacket(ctx context.Context, packet *bandtypes.P ) return fmt.Errorf("[EVMProvider] failed to estimate gas fee: %w", err) } - alert.HandleReset(cp.Alert, alert.NewTopic(alert.EstimateGasFeeErrorMsg).WithTunnelID(packet.TunnelID).WithChainName(cp.ChainName)) + alert.HandleReset( + cp.Alert, + alert.NewTopic(alert.EstimateGasFeeErrorMsg).WithTunnelID(packet.TunnelID).WithChainName(cp.ChainName), + ) var lastErr error var bumpGasErr error @@ -186,9 +189,19 @@ func (cp *EVMChainProvider) RelayPacket(ctx context.Context, packet *bandtypes.P continue } - balance, err := cp.Client.GetBalance(ctx, gethcommon.HexToAddress(freeSigner.GetAddress()), nil) - if err != nil { - log.Error("GetBalance error", err) + var balance *big.Int + if cp.DB != nil { + balance, err = cp.Client.GetBalance(ctx, gethcommon.HexToAddress(freeSigner.GetAddress()), nil) + if err != nil { + log.Error("Failed to get balance", "retry_count", retryCount, err) + alert.HandleAlert(cp.Alert, alert.NewTopic(alert.GetBalanceErrorMsg). + WithTunnelID(packet.TunnelID). + WithChainName(cp.ChainName), err.Error()) + } else { + alert.HandleReset(cp.Alert, alert.NewTopic(alert.GetBalanceErrorMsg). + WithTunnelID(packet.TunnelID). + WithChainName(cp.ChainName)) + } } // submit the transaction, if failed, bump gas and retry @@ -213,14 +226,28 @@ func (cp *EVMChainProvider) RelayPacket(ctx context.Context, packet *bandtypes.P "retry_count", retryCount, ) - if err := cp.saveUnconfirmedTransaction(txHash, types.TX_STATUS_PENDING, packet, freeSigner.GetAddress()); err != nil { - log.Error("SaveTransaction error", "retry_count", retryCount, err) + // save pending tx in db + if cp.DB != nil { + tx := cp.prepareTransaction(ctx, txHash, freeSigner.GetAddress(), packet, nil, balance, log, retryCount) + cp.handleSaveTransaction(tx, log, retryCount) } txResult := cp.WaitForConfirmedTx(ctx, txHash, log) cp.handleMetrics(packet.TunnelID, createdAt, txResult) - cp.handleSaveTransaction(ctx, freeSigner.GetAddress(), balance, packet, txResult, retryCount, log) + if cp.DB != nil { + tx := cp.prepareTransaction( + ctx, + txHash, + freeSigner.GetAddress(), + packet, + &txResult, + balance, + log, + retryCount, + ) + cp.handleSaveTransaction(tx, log, retryCount) + } if txResult.Status == types.TX_STATUS_SUCCESS { log.Info( @@ -336,7 +363,6 @@ func (cp *EVMChainProvider) WaitForConfirmedTx( } return NewTxResult( - txHash, types.TX_STATUS_TIMEOUT, decimal.NullDecimal{}, decimal.NullDecimal{}, @@ -370,25 +396,108 @@ func (cp *EVMChainProvider) handleMetrics(tunnelID uint64, createdAt time.Time, } } -// handleSaveTransaction saves the transaction to the database based on its status. -func (cp *EVMChainProvider) handleSaveTransaction(ctx context.Context, +// prepareTransaction prepares the transaction to be stored in the database. +func (cp *EVMChainProvider) prepareTransaction( + ctx context.Context, + txHash string, signerAddress string, - oldBalance *big.Int, packet *bandtypes.Packet, - txResult TxResult, - retryCount int, + txResult *TxResult, + oldBalance *big.Int, log logger.Logger, -) { - switch txResult.Status { - case types.TX_STATUS_SUCCESS, types.TX_STATUS_FAILED: - if err := cp.saveConfirmedTransaction(ctx, signerAddress, oldBalance, packet, txResult); err != nil { - log.Error("SaveTransaction error", "retry_count", retryCount, err) - } - default: - if err := cp.saveUnconfirmedTransaction(txResult.TxHash, txResult.Status, packet, signerAddress); err != nil { - log.Error("SaveTransaction error", "retry_count", retryCount, err) + retryCount int, +) *db.Transaction { + var signalPrices []db.SignalPrice + + for _, p := range packet.SignalPrices { + signalPrices = append(signalPrices, *db.NewSignalPrice(p.SignalID, p.Price)) + } + + txStatus := types.TX_STATUS_PENDING + gasUsed := decimal.NullDecimal{} + effectiveGasPrice := decimal.NullDecimal{} + balanceDelta := decimal.NullDecimal{} + + var blockTimestamp *time.Time + + if txResult != nil { + txStatus = txResult.Status + gasUsed = txResult.GasUsed + effectiveGasPrice = txResult.EffectiveGasPrice + + if txResult.Status == types.TX_STATUS_SUCCESS || txResult.Status == types.TX_STATUS_FAILED { + block, err := cp.Client.GetBlock(context.Background(), txResult.BlockNumber) + if err != nil { + log.Error("Failed to get block", "retry_count", retryCount, err) + alert.HandleAlert(cp.Alert, alert.NewTopic(alert.GetBlockErrorMsg). + WithTunnelID(packet.TunnelID). + WithChainName(cp.ChainName), err.Error()) + } else { + timestamp := time.Unix(int64(block.Time()), 0).UTC() + blockTimestamp = ×tamp + alert.HandleReset(cp.Alert, alert.NewTopic(alert.GetBlockErrorMsg). + WithTunnelID(packet.TunnelID). + WithChainName(cp.ChainName)) + } + + // Compute new balance + // Note: this may be incorrect if other transactions affected the user's balance during this period. + if oldBalance != nil { + newBalance, err := cp.Client.GetBalance( + ctx, + gethcommon.HexToAddress(signerAddress), + txResult.BlockNumber, + ) + if err != nil { + log.Error("Failed to get balance", "retry_count", retryCount, err) + alert.HandleAlert(cp.Alert, alert.NewTopic(alert.GetBalanceErrorMsg). + WithTunnelID(packet.TunnelID). + WithChainName(cp.ChainName), err.Error()) + } else { + diff := new(big.Int).Sub(newBalance, oldBalance) + balanceDelta = decimal.NewNullDecimal(decimal.NewFromBigInt(diff, 0)) + alert.HandleReset(cp.Alert, alert.NewTopic(alert.GetBalanceErrorMsg). + WithTunnelID(packet.TunnelID). + WithChainName(cp.ChainName)) + } + } } } + + tx := db.NewTransaction( + txHash, + packet.TunnelID, + packet.Sequence, + cp.ChainName, + types.ChainTypeEVM, + signerAddress, + txStatus, + gasUsed, + effectiveGasPrice, + balanceDelta, + signalPrices, + blockTimestamp, + ) + + return tx +} + +// handleSaveTransaction saves the transaction to the database and triggers alert if any error occurs. +func (cp *EVMChainProvider) handleSaveTransaction(tx *db.Transaction, log logger.Logger, retryCount int) { + if cp.DB == nil { + log.Debug("Database is not set; skipping saving transaction") + return + } + if err := cp.DB.AddOrUpdateTransaction(tx); err != nil { + log.Error("Save transaction error", "retry_count", retryCount, err) + alert.HandleAlert(cp.Alert, alert.NewTopic(alert.SaveDatabaseErrorMsg). + WithTunnelID(tx.TunnelID). + WithChainName(cp.ChainName), err.Error()) + } else { + alert.HandleReset(cp.Alert, alert.NewTopic(alert.SaveDatabaseErrorMsg). + WithTunnelID(tx.TunnelID). + WithChainName(cp.ChainName)) + } } // CheckConfirmedTx checks the confirmed transaction status. @@ -400,7 +509,6 @@ func (cp *EVMChainProvider) CheckConfirmedTx( if err != nil { err = fmt.Errorf("failed to get tx receipt: %w", err) return NewTxResult( - txHash, types.TX_STATUS_PENDING, decimal.NullDecimal{}, decimal.NullDecimal{}, @@ -415,7 +523,6 @@ func (cp *EVMChainProvider) CheckConfirmedTx( if receipt.Status == gethtypes.ReceiptStatusFailed { return NewTxResult( - txHash, types.TX_STATUS_FAILED, gasUsed, gasPrice, @@ -428,7 +535,6 @@ func (cp *EVMChainProvider) CheckConfirmedTx( if err != nil { err = fmt.Errorf("failed to get latest block height: %w", err) return NewTxResult( - txHash, types.TX_STATUS_PENDING, decimal.NullDecimal{}, decimal.NullDecimal{}, @@ -440,7 +546,6 @@ func (cp *EVMChainProvider) CheckConfirmedTx( // if tx block is not confirmed and waiting too long return status with timeout if receipt.BlockNumber.Uint64() > latestBlock-cp.Config.BlockConfirmation { return NewTxResult( - txHash, types.TX_STATUS_PENDING, decimal.NullDecimal{}, decimal.NullDecimal{}, @@ -449,7 +554,7 @@ func (cp *EVMChainProvider) CheckConfirmedTx( ), nil } - return NewTxResult(txHash, types.TX_STATUS_SUCCESS, gasUsed, gasPrice, receipt.BlockNumber, ""), nil + return NewTxResult(types.TX_STATUS_SUCCESS, gasUsed, gasPrice, receipt.BlockNumber, ""), nil } // EstimateGasFee estimates the gas for the transaction. @@ -765,98 +870,3 @@ func (cp *EVMChainProvider) queryRelayerGasFee(ctx context.Context) (*big.Int, e return output, nil } - -func (cp *EVMChainProvider) saveUnconfirmedTransaction( - txHash string, - txStatus types.TxStatus, - packet *bandtypes.Packet, - sender string, -) error { - // db was disabled - if cp.DB == nil { - return nil - } - - var signalPrices []db.SignalPrice - for _, p := range packet.SignalPrices { - signalPrices = append(signalPrices, *db.NewSignalPrice(p.SignalID, p.Price)) - } - - tx := db.NewUnconfirmedTransaction( - txHash, - packet.TunnelID, - packet.Sequence, - cp.ChainName, - types.ChainTypeEVM, - sender, - txStatus, - signalPrices, - ) - - if err := cp.DB.AddOrUpdateTransaction(tx); err != nil { - return fmt.Errorf("failed to save transaction to database: %w", err) - } - - return nil -} - -// saveConfirmedTransaction stores the transaction result and related metadata (e.g. gas, status, balance delta) to the database if enabled. -func (cp *EVMChainProvider) saveConfirmedTransaction( - ctx context.Context, - signerAddress string, - oldBalance *big.Int, - packet *bandtypes.Packet, - txResult TxResult, -) error { - // db was disabled - if cp.DB == nil { - return nil - } - - var signalPrices []db.SignalPrice - for _, p := range packet.SignalPrices { - signalPrices = append(signalPrices, *db.NewSignalPrice(p.SignalID, p.Price)) - } - - var blockTimestamp time.Time - balanceDelta := decimal.NullDecimal{} - - block, err := cp.Client.GetBlock(ctx, txResult.BlockNumber) - if err != nil { - return fmt.Errorf("failed to get block: %w", err) - } - - blockTimestamp = time.Unix(int64(block.Time()), 0).UTC() - - // Compute new balance - // Note: this may be incorrect if other transactions affected the user's balance during this period. - if oldBalance != nil { - newBalance, err := cp.Client.GetBalance(ctx, gethcommon.HexToAddress(signerAddress), txResult.BlockNumber) - if err != nil { - return fmt.Errorf("failed to get balance: %w", err) - } - diff := new(big.Int).Sub(newBalance, oldBalance) - balanceDelta = decimal.NewNullDecimal(decimal.NewFromBigInt(diff, 0)) - } - - tx := db.NewConfirmedTransaction( - txResult.TxHash, - packet.TunnelID, - packet.Sequence, - cp.ChainName, - types.ChainTypeEVM, - signerAddress, - txResult.Status, - txResult.GasUsed, - txResult.EffectiveGasPrice, - balanceDelta, - signalPrices, - blockTimestamp, - ) - - if err := cp.DB.AddOrUpdateTransaction(tx); err != nil { - return fmt.Errorf("failed to save transaction to database: %w", err) - } - - return nil -} diff --git a/relayer/chains/evm/provider_eip1559_test.go b/relayer/chains/evm/provider_eip1559_test.go index 21e4b5b..20c2ee2 100644 --- a/relayer/chains/evm/provider_eip1559_test.go +++ b/relayer/chains/evm/provider_eip1559_test.go @@ -107,7 +107,6 @@ func (s *EIP1559ProviderTestSuite) TestRelayPacketSuccess() { GasTipCap: s.gasInfo.GasPriorityFee, }).Return(uint64(200_000), nil) - s.client.EXPECT().GetBalance(gomock.Any(), s.mockSignerAddress, nil).Return(big.NewInt(10000), nil) txHash := "0xabc123" s.client.EXPECT().BroadcastTx(gomock.Any(), gomock.Any()).Return(txHash, nil) s.client.EXPECT().GetTxReceipt(gomock.Any(), txHash).Return(&evm.TxReceipt{ @@ -137,7 +136,6 @@ func (s *EIP1559ProviderTestSuite) TestRelayPacketSuccessWithoutQueryMaxGasFee() GasTipCap: big.NewInt(3_000_000_000), }).Return(uint64(200_000), nil) - s.client.EXPECT().GetBalance(gomock.Any(), s.mockSignerAddress, nil).Return(big.NewInt(10000), nil) txHash := "0xabc123" s.client.EXPECT().BroadcastTx(gomock.Any(), gomock.Any()).Return(txHash, nil) s.client.EXPECT().GetTxReceipt(gomock.Any(), txHash).Return(&evm.TxReceipt{ @@ -182,8 +180,6 @@ func (s *EIP1559ProviderTestSuite) TestRelayPacketFailedBroadcastTx() { GasTipCap: s.gasInfo.GasPriorityFee, }).Return(uint64(200_000), nil).Times(s.chainProvider.Config.MaxRetry) - s.client.EXPECT().GetBalance(gomock.Any(), s.mockSignerAddress, nil).Return(big.NewInt(10000), nil). - Times(s.chainProvider.Config.MaxRetry) s.client.EXPECT(). BroadcastTx(gomock.Any(), gomock.Any()). Return("", fmt.Errorf("failed to broadcast an evm transaction")). @@ -204,8 +200,6 @@ func (s *EIP1559ProviderTestSuite) TestRelayPacketFailedTxReceiptStatus() { GasTipCap: s.gasInfo.GasPriorityFee, }).Return(uint64(200_000), nil).Times(s.chainProvider.Config.MaxRetry) - s.client.EXPECT().GetBalance(gomock.Any(), s.mockSignerAddress, nil).Return(big.NewInt(10000), nil). - Times(s.chainProvider.Config.MaxRetry) txHash := "0xabc123" s.client.EXPECT(). BroadcastTx(gomock.Any(), gomock.Any()). diff --git a/relayer/chains/evm/provider_legacy_test.go b/relayer/chains/evm/provider_legacy_test.go index eb5817f..ac4fac7 100644 --- a/relayer/chains/evm/provider_legacy_test.go +++ b/relayer/chains/evm/provider_legacy_test.go @@ -105,7 +105,6 @@ func (s *LegacyProviderTestSuite) TestRelayPacketSuccess() { GasPrice: s.gasInfo.GasPrice, }).Return(uint64(200_000), nil).AnyTimes() - s.client.EXPECT().GetBalance(gomock.Any(), s.mockSignerAddress, nil).Return(big.NewInt(10000), nil) txHash := "0xabc123" s.client.EXPECT().BroadcastTx(gomock.Any(), gomock.Any()).Return(txHash, nil) s.client.EXPECT().GetTxReceipt(gomock.Any(), txHash).Return(&evm.TxReceipt{ @@ -133,7 +132,6 @@ func (s *LegacyProviderTestSuite) TestRelayPacketSuccessWithoutQueryMaxGasFee() GasPrice: big.NewInt(2_000_000_000), }).Return(uint64(200_000), nil) - s.client.EXPECT().GetBalance(gomock.Any(), s.mockSignerAddress, nil).Return(big.NewInt(10000), nil) txHash := "0xabc123" s.client.EXPECT().BroadcastTx(gomock.Any(), gomock.Any()).Return(txHash, nil) s.client.EXPECT().GetTxReceipt(gomock.Any(), txHash).Return(&evm.TxReceipt{ diff --git a/relayer/chains/evm/provider_test.go b/relayer/chains/evm/provider_test.go index d6d3c84..c9c839a 100644 --- a/relayer/chains/evm/provider_test.go +++ b/relayer/chains/evm/provider_test.go @@ -233,7 +233,6 @@ func (s *ProviderTestSuite) TestCheckConfirmedTx() { s.client.EXPECT().GetBlockHeight(gomock.Any()).Return(uint64(currentBlock), nil) }, out: evm.NewTxResult( - txHash, chaintypes.TX_STATUS_SUCCESS, decimal.NewNullDecimal(decimal.New(21000, 0)), decimal.NewNullDecimal(decimal.New(20000, 0)), @@ -252,7 +251,6 @@ func (s *ProviderTestSuite) TestCheckConfirmedTx() { }, nil) }, out: evm.NewTxResult( - txHash, chaintypes.TX_STATUS_FAILED, decimal.NewNullDecimal(decimal.New(21000, 0)), decimal.NewNullDecimal(decimal.New(20000, 0)), @@ -274,7 +272,6 @@ func (s *ProviderTestSuite) TestCheckConfirmedTx() { s.client.EXPECT().GetBlockHeight(gomock.Any()).Return(uint64(currentBlock), nil) }, out: evm.NewTxResult( - txHash, chaintypes.TX_STATUS_PENDING, decimal.NullDecimal{}, decimal.NullDecimal{}, diff --git a/relayer/chains/evm/types.go b/relayer/chains/evm/types.go index 7a0c62a..ba6e678 100644 --- a/relayer/chains/evm/types.go +++ b/relayer/chains/evm/types.go @@ -37,7 +37,6 @@ type txResultMarshaling struct { // TxResult is the result of transaction. type TxResult struct { - TxHash string Status types.TxStatus GasUsed decimal.NullDecimal EffectiveGasPrice decimal.NullDecimal @@ -49,7 +48,6 @@ type TxResult struct { // NewTxResult creates a new TxResult instance. func NewTxResult( - txHash string, status types.TxStatus, gasUsed decimal.NullDecimal, effectiveGasPrice decimal.NullDecimal, @@ -57,7 +55,6 @@ func NewTxResult( failureReason string, ) TxResult { return TxResult{ - TxHash: txHash, Status: status, GasUsed: gasUsed, EffectiveGasPrice: effectiveGasPrice, diff --git a/relayer/db/types.go b/relayer/db/types.go index bc03d94..49736ba 100644 --- a/relayer/db/types.go +++ b/relayer/db/types.go @@ -23,36 +23,13 @@ type Transaction struct { BalanceDelta decimal.NullDecimal `gorm:"type:decimal"` SignalPrices []SignalPrice - BlockTimestamp time.Time `gorm:"default:NULL"` + BlockTimestamp *time.Time `gorm:"default:NULL"` CreatedAt time.Time UpdatedAt time.Time } -// NewUnconfirmedTransaction creates a new pending Transaction instance. -func NewUnconfirmedTransaction( - txHash string, - tunnelID uint64, - sequence uint64, - chainName string, - chainType types.ChainType, - sender string, - status types.TxStatus, - signalPrices []SignalPrice, -) *Transaction { - return &Transaction{ - TxHash: txHash, - TunnelID: tunnelID, - Sequence: sequence, - ChainName: chainName, - ChainType: chainType, - Sender: sender, - Status: status, - SignalPrices: signalPrices, - } -} - -// NewConfirmedTransaction creates a new Transaction instance. -func NewConfirmedTransaction( +// NewTransaction creates a new Transaction instance. +func NewTransaction( txHash string, tunnelID uint64, sequence uint64, @@ -64,7 +41,7 @@ func NewConfirmedTransaction( effectiveGasPrice decimal.NullDecimal, balanceDelta decimal.NullDecimal, signalPrices []SignalPrice, - blockTimestamp time.Time, + blockTimestamp *time.Time, ) *Transaction { return &Transaction{ TxHash: txHash,