diff --git a/errors.go b/errors.go index f9148882..62b664b3 100644 --- a/errors.go +++ b/errors.go @@ -49,6 +49,7 @@ var errParameterNull = errors.New("the parameter can't be null") var errNetworkNameMissing = errors.New("can't derive checksum for ID without knowing which _Network the ID is for") var errChecksumMissing = errors.New("no checksum provided") var errLockedSlice = errors.New("slice is locked") +var errNodeIsUnhealthy = errors.New("node is unhealthy") type ErrInvalidNodeAccountIDSet struct { NodeAccountID AccountID diff --git a/executable.go b/executable.go index 3cfc2c95..100f4813 100644 --- a/executable.go +++ b/executable.go @@ -246,7 +246,7 @@ func _Execute(client *Client, e Executable) (interface{}, error) { if !node._IsHealthy() { txLogger.Trace("node is unhealthy, waiting before continuing", "requestId", e.getLogID(e), "delay", node._Wait().String()) - _DelayForAttempt(e.getLogID(e), currentBackoff, attempt, txLogger) + _DelayForAttempt(e.getLogID(e), currentBackoff, attempt, txLogger, errNodeIsUnhealthy) continue } @@ -325,7 +325,7 @@ func _Execute(client *Client, e Executable) (interface{}, error) { switch e.shouldRetry(e, resp) { case executionStateRetry: errPersistent = statusError - _DelayForAttempt(e.getLogID(e), currentBackoff, attempt, txLogger) + _DelayForAttempt(e.getLogID(e), currentBackoff, attempt, txLogger, errPersistent) continue case executionStateExpired: if e.isTransaction() { @@ -364,8 +364,8 @@ func _Execute(client *Client, e Executable) (interface{}, error) { return &services.Response{}, errPersistent } -func _DelayForAttempt(logID string, backoff time.Duration, attempt int64, logger Logger) { - logger.Trace("retrying request attempt", "requestId", logID, "delay", backoff, "attempt", attempt+1) +func _DelayForAttempt(logID string, backoff time.Duration, attempt int64, logger Logger, err error) { + logger.Trace("retrying request attempt", "requestId", logID, "delay", backoff, "attempt", attempt+1, "error", err) time.Sleep(backoff) } diff --git a/query.go b/query.go index e6bf3048..df6065c9 100644 --- a/query.go +++ b/query.go @@ -263,7 +263,6 @@ func (q *Query) shouldRetry(e Executable, response interface{}) _ExecutionState StatusPlatformTransactionNotCreated: true, StatusPlatformNotActive: true, StatusBusy: true, - StatusThrottledAtConsensus: true, } if retryableStatuses[status] { diff --git a/schedule_create_transaction_e2e_test.go b/schedule_create_transaction_e2e_test.go index bdf92207..8c291a09 100644 --- a/schedule_create_transaction_e2e_test.go +++ b/schedule_create_transaction_e2e_test.go @@ -56,6 +56,7 @@ func TestIntegrationScheduleCreateTransactionCanExecute(t *testing.T) { require.NoError(t, err) transactionReceipt, err := createResponse.SetValidateStatus(true).GetReceipt(env.Client) + require.NoError(t, err) transactionID := TransactionIDGenerate(env.OperatorID) newAccountID := *transactionReceipt.AccountID diff --git a/transaction.go b/transaction.go index f189a595..fe982bd7 100644 --- a/transaction.go +++ b/transaction.go @@ -4578,6 +4578,16 @@ func TransactionToBytes(transaction interface{}) ([]byte, error) { // nolint return i.ToBytes() case *TransferTransaction: return i.ToBytes() + case *TokenUpdateNfts: + return i.ToBytes() + case *TokenRejectTransaction: + return i.ToBytes() + case *TokenAirdropTransaction: + return i.ToBytes() + case *TokenCancelAirdropTransaction: + return i.ToBytes() + case *TokenClaimAirdropTransaction: + return i.ToBytes() default: return nil, errors.New("(BUG) non-exhaustive switch statement") } @@ -4749,6 +4759,16 @@ func TransactionExecute(transaction interface{}, client *Client) (TransactionRes return i.Execute(client) case *TransferTransaction: return i.Execute(client) + case *TokenUpdateNfts: + return i.Execute(client) + case *TokenRejectTransaction: + return i.Execute(client) + case *TokenAirdropTransaction: + return i.Execute(client) + case *TokenCancelAirdropTransaction: + return i.Execute(client) + case *TokenClaimAirdropTransaction: + return i.Execute(client) default: return TransactionResponse{}, errors.New("(BUG) non-exhaustive switch statement") } @@ -4762,7 +4782,6 @@ func (tx *Transaction) shouldRetry(_ Executable, response interface{}) _Executio StatusPlatformTransactionNotCreated: true, StatusPlatformNotActive: true, StatusBusy: true, - StatusThrottledAtConsensus: true, } if retryableStatuses[status] { @@ -4899,12 +4918,16 @@ func (tx *Transaction) execute(client *Client, e TransactionInterface) (Transact ValidateStatus: true, }, err } - + originalTxID := tx.GetTransactionID() + e.regenerateID(client) return TransactionResponse{ - TransactionID: tx.GetTransactionID(), + TransactionID: originalTxID, NodeID: resp.(TransactionResponse).NodeID, Hash: resp.(TransactionResponse).Hash, ValidateStatus: true, + // set the tx in the response, in case of throttle error in the receipt + // we can use this to re-submit the transaction + Transaction: e, }, nil } diff --git a/transaction_receipt_query.go b/transaction_receipt_query.go index 76449187..d22e2b75 100644 --- a/transaction_receipt_query.go +++ b/transaction_receipt_query.go @@ -242,46 +242,25 @@ func (q *TransactionReceiptQuery) validateNetworkOnIDs(client *Client) error { } func (q *TransactionReceiptQuery) shouldRetry(_ Executable, response interface{}) _ExecutionState { - receiptResponse := response.(*services.Response).GetTransactionGetReceipt() - header := receiptResponse.GetHeader() - - status := Status(header.GetNodeTransactionPrecheckCode()) - - retryableHeaderStatuses := map[Status]bool{ - StatusPlatformTransactionNotCreated: true, - StatusBusy: true, - StatusUnknown: true, - StatusReceiptNotFound: true, - StatusRecordNotFound: true, - StatusPlatformNotActive: true, - StatusThrottledAtConsensus: true, - } + status := Status(response.(*services.Response).GetTransactionGetReceipt().GetHeader().GetNodeTransactionPrecheckCode()) - if retryableHeaderStatuses[status] { + switch status { + case StatusPlatformTransactionNotCreated, StatusBusy, StatusUnknown, StatusReceiptNotFound, StatusRecordNotFound, StatusPlatformNotActive: return executionStateRetry - } - - if status != StatusOk { + case StatusOk: + break + default: return executionStateError } - status = Status(receiptResponse.GetReceipt().GetStatus()) - - retryableReceiptStatuses := map[Status]bool{ - StatusBusy: true, - StatusUnknown: true, - StatusOk: true, - StatusReceiptNotFound: true, - StatusRecordNotFound: true, - StatusPlatformNotActive: true, - StatusThrottledAtConsensus: true, - } + status = Status(response.(*services.Response).GetTransactionGetReceipt().GetReceipt().GetStatus()) - if retryableReceiptStatuses[status] { + switch status { + case StatusBusy, StatusUnknown, StatusOk, StatusReceiptNotFound, StatusRecordNotFound, StatusPlatformNotActive: return executionStateRetry + default: + return executionStateFinished } - - return executionStateFinished } func (q *TransactionReceiptQuery) getQueryResponse(response *services.Response) queryResponse { diff --git a/transaction_receipt_query_unit_test.go b/transaction_receipt_query_unit_test.go index 840b0c7f..7de09e12 100644 --- a/transaction_receipt_query_unit_test.go +++ b/transaction_receipt_query_unit_test.go @@ -115,53 +115,6 @@ func TestUnitTransactionReceiptQueryNothingSet(t *testing.T) { balance.GetMaxQueryPayment() } -func TestUnitTransactionReceiptThrottledAtConsensusGracefulHandling(t *testing.T) { - t.Parallel() - - responses := [][]interface{}{{ - &services.TransactionResponse{ - NodeTransactionPrecheckCode: services.ResponseCodeEnum_OK, - }, - &services.Response{ - Response: &services.Response_TransactionGetReceipt{ - TransactionGetReceipt: &services.TransactionGetReceiptResponse{ - Header: &services.ResponseHeader{ - Cost: 0, - ResponseType: services.ResponseType_ANSWER_ONLY, - }, - Receipt: &services.TransactionReceipt{ - Status: services.ResponseCodeEnum_THROTTLED_AT_CONSENSUS, - }, - }, - }, - }, - &services.Response{ - Response: &services.Response_TransactionGetReceipt{ - TransactionGetReceipt: &services.TransactionGetReceiptResponse{ - Header: &services.ResponseHeader{ - Cost: 0, - ResponseType: services.ResponseType_ANSWER_ONLY, - }, - Receipt: &services.TransactionReceipt{ - Status: services.ResponseCodeEnum_SUCCESS, - }, - }, - }, - }, - }} - client, server := NewMockClientAndServer(responses) - defer server.Close() - tx, err := NewTransferTransaction(). - SetNodeAccountIDs([]AccountID{{Account: 3}}). - AddHbarTransfer(AccountID{Account: 2}, HbarFromTinybar(-1)). - AddHbarTransfer(AccountID{Account: 3}, HbarFromTinybar(1)). - Execute(client) - client.SetMaxAttempts(2) - require.NoError(t, err) - _, err = tx.SetValidateStatus(true).GetReceipt(client) - require.NoError(t, err) -} - func TestUnitTransactionPlatformNotActiveGracefulHandling(t *testing.T) { t.Parallel() diff --git a/transaction_record_query.go b/transaction_record_query.go index 5d8e6c4e..4f4075f8 100644 --- a/transaction_record_query.go +++ b/transaction_record_query.go @@ -247,50 +247,29 @@ func (q *TransactionRecordQuery) validateNetworkOnIDs(client *Client) error { } func (q *TransactionRecordQuery) shouldRetry(_ Executable, response interface{}) _ExecutionState { - record := response.(*services.Response).GetTransactionGetRecord() - header := record.GetHeader() - - status := Status(header.GetNodeTransactionPrecheckCode()) - - retryableHeaderStatuses := map[Status]bool{ - StatusPlatformTransactionNotCreated: true, - StatusBusy: true, - StatusUnknown: true, - StatusReceiptNotFound: true, - StatusRecordNotFound: true, - StatusPlatformNotActive: true, - StatusThrottledAtConsensus: true, - } + status := Status(response.(*services.Response).GetTransactionGetRecord().GetHeader().GetNodeTransactionPrecheckCode()) - if retryableHeaderStatuses[status] { + switch status { + case StatusPlatformTransactionNotCreated, StatusBusy, StatusUnknown, StatusReceiptNotFound, StatusRecordNotFound, StatusPlatformNotActive: return executionStateRetry + case StatusOk: + if response.(*services.Response).GetTransactionGetRecord().GetHeader().ResponseType == services.ResponseType_COST_ANSWER { + return executionStateFinished + } + default: + return executionStateError } - if status == StatusOk && header.ResponseType == services.ResponseType_COST_ANSWER { - return executionStateFinished - } - - status = Status(record.GetTransactionRecord().GetReceipt().GetStatus()) + status = Status(response.(*services.Response).GetTransactionGetRecord().GetTransactionRecord().GetReceipt().GetStatus()) - retryableReceiptStatuses := map[Status]bool{ - StatusBusy: true, - StatusUnknown: true, - StatusOk: true, - StatusReceiptNotFound: true, - StatusRecordNotFound: true, - StatusPlatformNotActive: true, - StatusThrottledAtConsensus: true, - } - - if retryableReceiptStatuses[status] { + switch status { + case StatusBusy, StatusUnknown, StatusOk, StatusReceiptNotFound, StatusRecordNotFound, StatusPlatformNotActive: return executionStateRetry - } - - if status == StatusSuccess { + case StatusSuccess: return executionStateFinished + default: + return executionStateError } - - return executionStateError } func (q *TransactionRecordQuery) getQueryResponse(response *services.Response) queryResponse { diff --git a/transaction_record_query_unit_test.go b/transaction_record_query_unit_test.go index 4f62caa4..6e6bcc47 100644 --- a/transaction_record_query_unit_test.go +++ b/transaction_record_query_unit_test.go @@ -123,101 +123,6 @@ func TestUnitTransactionRecordQueryNothingSet(t *testing.T) { require.Equal(t, Hbar{}, query.GetMaxQueryPayment()) } -func TestUnitTransactionRecordThrottledAtConsensusGracefulHandling(t *testing.T) { - t.Parallel() - - responses := [][]interface{}{{ - &services.TransactionResponse{ - NodeTransactionPrecheckCode: services.ResponseCodeEnum_OK, - }, - &services.Response{ - Response: &services.Response_TransactionGetReceipt{ - TransactionGetReceipt: &services.TransactionGetReceiptResponse{ - Header: &services.ResponseHeader{ - Cost: 0, - ResponseType: services.ResponseType_ANSWER_ONLY, - }, - Receipt: &services.TransactionReceipt{ - Status: services.ResponseCodeEnum_SUCCESS, - }, - }, - }, - }, - &services.Response{ - Response: &services.Response_TransactionGetRecord{ - TransactionGetRecord: &services.TransactionGetRecordResponse{ - Header: &services.ResponseHeader{ - Cost: 0, - ResponseType: services.ResponseType_ANSWER_ONLY, - }, - TransactionRecord: &services.TransactionRecord{ - Receipt: &services.TransactionReceipt{ - Status: services.ResponseCodeEnum_THROTTLED_AT_CONSENSUS, - }, - }, - }, - }, - }, - &services.Response{ - Response: &services.Response_TransactionGetRecord{ - TransactionGetRecord: &services.TransactionGetRecordResponse{ - Header: &services.ResponseHeader{ - Cost: 0, - ResponseType: services.ResponseType_ANSWER_ONLY, - }, - TransactionRecord: &services.TransactionRecord{ - Receipt: &services.TransactionReceipt{ - Status: services.ResponseCodeEnum_SUCCESS, - }, - }, - }, - }, - }, - &services.Response{ - Response: &services.Response_TransactionGetRecord{ - TransactionGetRecord: &services.TransactionGetRecordResponse{ - Header: &services.ResponseHeader{ - Cost: 0, - ResponseType: services.ResponseType_ANSWER_ONLY, - }, - TransactionRecord: &services.TransactionRecord{ - Receipt: &services.TransactionReceipt{ - Status: services.ResponseCodeEnum_THROTTLED_AT_CONSENSUS, - }, - }, - }, - }, - }, - &services.Response{ - Response: &services.Response_TransactionGetRecord{ - TransactionGetRecord: &services.TransactionGetRecordResponse{ - Header: &services.ResponseHeader{ - Cost: 0, - ResponseType: services.ResponseType_ANSWER_ONLY, - }, - TransactionRecord: &services.TransactionRecord{ - Receipt: &services.TransactionReceipt{ - Status: services.ResponseCodeEnum_SUCCESS, - }, - }, - }, - }, - }, - }} - - client, server := NewMockClientAndServer(responses) - defer server.Close() - tx, err := NewTransferTransaction(). - SetNodeAccountIDs([]AccountID{{Account: 3}}). - AddHbarTransfer(AccountID{Account: 2}, HbarFromTinybar(-1)). - AddHbarTransfer(AccountID{Account: 3}, HbarFromTinybar(1)). - Execute(client) - client.SetMaxAttempts(2) - require.NoError(t, err) - _, err = tx.SetValidateStatus(true).GetRecord(client) - require.NoError(t, err) -} - func TestUnitTransactionRecordPlatformNotActiveGracefulHandling(t *testing.T) { t.Parallel() diff --git a/transaction_response.go b/transaction_response.go index bce33b82..7025bf9e 100644 --- a/transaction_response.go +++ b/transaction_response.go @@ -37,6 +37,7 @@ type TransactionResponse struct { NodeID AccountID Hash []byte ValidateStatus bool + Transaction TransactionInterface } // MarshalJSON returns the JSON representation of the TransactionResponse. @@ -50,6 +51,19 @@ func (response TransactionResponse) MarshalJSON() ([]byte, error) { return json.Marshal(obj) } +// retryTransaction is a helper function to retry a transaction that was throttled +func retryTransaction(client *Client, transaction TransactionInterface) (TransactionReceipt, error) { + resp, err := TransactionExecute(transaction, client) + if err != nil { + return TransactionReceipt{}, err + } + receipt, err := NewTransactionReceiptQuery(). + SetTransactionID(resp.TransactionID). + SetNodeAccountIDs([]AccountID{resp.NodeID}). + Execute(client) + return receipt, err +} + // GetReceipt retrieves the receipt for the transaction func (response TransactionResponse) GetReceipt(client *Client) (TransactionReceipt, error) { receipt, err := NewTransactionReceiptQuery(). @@ -57,6 +71,10 @@ func (response TransactionResponse) GetReceipt(client *Client) (TransactionRecei SetNodeAccountIDs([]AccountID{response.NodeID}). Execute(client) + for receipt.Status == StatusThrottledAtConsensus { + receipt, err = retryTransaction(client, response.Transaction) + } + if err != nil { return receipt, err } diff --git a/transaction_unit_test.go b/transaction_unit_test.go index 0d5a6a95..406ca9b0 100644 --- a/transaction_unit_test.go +++ b/transaction_unit_test.go @@ -602,32 +602,6 @@ func TestUnitTransactionSignSwitchCasesPointers(t *testing.T) { } } -func TestUnitTransactionThrottleAtConsensusGracefulHandling(t *testing.T) { - t.Parallel() - - responses := [][]interface{}{{ - &services.TransactionResponse{ - NodeTransactionPrecheckCode: services.ResponseCodeEnum_THROTTLED_AT_CONSENSUS, - }, - &services.TransactionResponse{ - NodeTransactionPrecheckCode: services.ResponseCodeEnum_THROTTLED_AT_CONSENSUS, - }, - &services.TransactionResponse{ - NodeTransactionPrecheckCode: services.ResponseCodeEnum_OK, - }, - }} - - client, server := NewMockClientAndServer(responses) - defer server.Close() - _, err := NewTransferTransaction(). - SetNodeAccountIDs([]AccountID{{Account: 3}}). - AddHbarTransfer(AccountID{Account: 2}, HbarFromTinybar(-1)). - AddHbarTransfer(AccountID{Account: 3}, HbarFromTinybar(1)). - Execute(client) - client.SetMaxAttempts(3) - require.NoError(t, err) - -} func TestUnitTransactionAttributes(t *testing.T) { t.Parallel()