diff --git a/server/services/constants.go b/server/services/constants.go index 0f89056..0e3fab5 100644 --- a/server/services/constants.go +++ b/server/services/constants.go @@ -6,6 +6,7 @@ import ( "time" "github.com/multiversx/mx-chain-core-go/core" + vmcommon "github.com/multiversx/mx-chain-vm-common-go" ) var ( @@ -48,9 +49,15 @@ const ( transactionEventDataExecuteOnDestContext = "ExecuteOnDestContext" transactionEventDataAsyncCall = "AsyncCall" + transactionEventDataAsyncCallback = "AsyncCallback" transactionEventDataTransferAndExecute = "TransferAndExecute" ) +const ( + errorCodeUserError = int(vmcommon.UserError) + numElementsInAdditionalDataAsyncCallbackWithError = 4 +) + const ( numTopicsOfEventESDTTransfer = 4 numTopicsPerTransferOfEventMultiESDTNFTTransfer = 3 diff --git a/server/services/testdata/blocks_with_esdt_transfer_and_error.json b/server/services/testdata/blocks_with_esdt_transfer_and_error.json new file mode 100644 index 0000000..dccd502 --- /dev/null +++ b/server/services/testdata/blocks_with_esdt_transfer_and_error.json @@ -0,0 +1,229 @@ +[ + { + "comment": "block with ESDT transfer (fungible) and error", + "miniBlocks": [ + { + "hash": "25906b7e92179c39a494a680ae771aae9db6bb60f489edde5ce158e71ed72046", + "type": "TxBlock", + "processingType": "Normal", + "constructionState": "Final", + "sourceShard": 1, + "destinationShard": 1, + "transactions": [ + { + "type": "normal", + "processingTypeOnSource": "BuiltInFunctionCall", + "processingTypeOnDestination": "SCInvoking", + "hash": "dd59dbfb06682d18a8a8571dd7039d388ee0f44ba6438022426ba4f0a6abf319", + "nonce": 1051, + "round": 29791109, + "epoch": 2068, + "value": "0", + "receiver": "erd1qqqqqqqqqqqqqpgq4eehfw7kfnc8x9cf9nfejgmtknuz6kygx7tsvhn3uc", + "sender": "erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67", + "gasPrice": 1000000000, + "gasLimit": 25000000, + "data": "RVNEVFRyYW5zZmVyQDU3NDU0NzRjNDQyZDYyNjQzNDY0MzczOUA4YWM3MjMwNDg5ZTgwMDAwQDY1Nzg2NTYzNzU3NDY1NDE3MjYyNjk3NDcyNjE2NzY1QDdmZDNiZGQyMDI0MjdjMDBANTg0NTQ3NGM0NDJkNjUzNDMxMzM2NTY0", + "signature": "523b11c9ad97873ca5ad8201322af26a065feac5368ab3011f74647469f5edb720e993d181fd9abca18c183b4d88793c4d395e30551fd173005a6fe419b0240c", + "sourceShard": 1, + "destinationShard": 1, + "miniblockType": "TxBlock", + "miniblockHash": "25906b7e92179c39a494a680ae771aae9db6bb60f489edde5ce158e71ed72046", + "logs": { + "address": "erd1qqqqqqqqqqqqqpgq4eehfw7kfnc8x9cf9nfejgmtknuz6kygx7tsvhn3uc", + "events": [ + { + "address": "erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67", + "identifier": "ESDTTransfer", + "topics": [ + "V0VHTEQtYmQ0ZDc5", + "", + "iscjBInoAAA=", + "AAAAAAAAAAAFAK5zdLvWTPBzFwks05kja7T4LViIN5c=" + ], + "data": null, + "additionalData": [ + "", + "RVNEVFRyYW5zZmVy", + "V0VHTEQtYmQ0ZDc5", + "iscjBInoAAA=", + "ZXhlY3V0ZUFyYml0cmFnZQ==", + "f9O90gJCfAA=", + "WEVHTEQtZTQxM2Vk" + ] + }, + { + "address": "erd1qqqqqqqqqqqqqpgq4eehfw7kfnc8x9cf9nfejgmtknuz6kygx7tsvhn3uc", + "identifier": "ESDTTransfer", + "topics": [ + "V0VHTEQtYmQ0ZDc5", + "", + "iscjBInoAAA=", + "AAAAAAAAAAAFAGyiHzN/2mg3jFVMfE9AtTqQDqGRVIM=" + ], + "data": "QXN5bmNDYWxs", + "additionalData": [ + "QXN5bmNDYWxs", + "RVNEVFRyYW5zZmVy", + "V0VHTEQtYmQ0ZDc5", + "iscjBInoAAA=", + "c3dhcFRva2Vuc0ZpeGVkSW5wdXQ=", + "WEVHTEQtZTQxM2Vk", + "f9O90gJCfAA=" + ] + }, + { + "address": "erd1qqqqqqqqqqqqqpgqdj3p7vmlmf5r0rz4f37y7s9482gqagv32jpsj8g69g", + "identifier": "transferValueOnly", + "topics": [ + "", + "AAAAAAAAAAAFAK5zdLvWTPBzFwks05kja7T4LViIN5c=" + ], + "data": "QXN5bmNDYWxsYmFjaw==", + "additionalData": [ + "QXN5bmNDYWxsYmFjaw==", + "Y2FsbEJhY2s=", + "BA==", + "U2xpcHBhZ2UgZXhjZWVkZWQ=" + ] + }, + { + "address": "erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67", + "identifier": "internalVMErrors", + "topics": [ + "AAAAAAAAAAAFAK5zdLvWTPBzFwks05kja7T4LViIN5c=", + "ZXhlY3V0ZUFyYml0cmFnZQ==" + ], + "data": "CglydW50aW1lLmdvOjg0NCBbZXJyb3Igc2lnbmFsbGVkIGJ5IHNtYXJ0Y29udHJhY3RdIFtzd2FwVG9rZW5zRml4ZWRJbnB1dF0KCXJ1bnRpbWUuZ286ODQ0IFtlcnJvciBzaWduYWxsZWQgYnkgc21hcnRjb250cmFjdF0gW3N3YXBUb2tlbnNGaXhlZElucHV0XQoJcnVudGltZS5nbzo4NDQgW2Vycm9yIHNpZ25hbGxlZCBieSBzbWFydGNvbnRyYWN0XSBbc3dhcFRva2Vuc0ZpeGVkSW5wdXRdCglydW50aW1lLmdvOjg0MSBbU2xpcHBhZ2UgZXhjZWVkZWRd", + "additionalData": [ + "CglydW50aW1lLmdvOjg0NCBbZXJyb3Igc2lnbmFsbGVkIGJ5IHNtYXJ0Y29udHJhY3RdIFtzd2FwVG9rZW5zRml4ZWRJbnB1dF0KCXJ1bnRpbWUuZ286ODQ0IFtlcnJvciBzaWduYWxsZWQgYnkgc21hcnRjb250cmFjdF0gW3N3YXBUb2tlbnNGaXhlZElucHV0XQoJcnVudGltZS5nbzo4NDQgW2Vycm9yIHNpZ25hbGxlZCBieSBzbWFydGNvbnRyYWN0XSBbc3dhcFRva2Vuc0ZpeGVkSW5wdXRdCglydW50aW1lLmdvOjg0MSBbU2xpcHBhZ2UgZXhjZWVkZWRd" + ] + }, + { + "address": "erd1qqqqqqqqqqqqqpgq4eehfw7kfnc8x9cf9nfejgmtknuz6kygx7tsvhn3uc", + "identifier": "completedTxEvent", + "topics": [ + "3Vnb+wZoLRioqFcd1wOdOI7g9EumQ4AiQmuk8Kar8xk=" + ], + "data": null, + "additionalData": null + } + ] + }, + "status": "success", + "tokens": [ + "WEGLD-bd4d79" + ], + "esdtValues": [ + "10000000000000000000" + ], + "operation": "ESDTTransfer", + "function": "executeArbitrage", + "initiallyPaidFee": "491065000000000", + "chainID": "1", + "version": 2, + "options": 0 + } + ], + "indexOfFirstTxProcessed": 0, + "indexOfLastTxProcessed": 39 + }, + { + "hash": "9ffbaf706ac92b240eecc2934a8f7449093a5472db6b8010dfb48821028557f1", + "type": "SmartContractResultBlock", + "processingType": "Normal", + "isFromReceiptsStorage": true, + "sourceShard": 1, + "destinationShard": 1, + "transactions": [ + { + "type": "unsigned", + "processingTypeOnSource": "SCInvoking", + "processingTypeOnDestination": "SCInvoking", + "hash": "0be42df46e8908ad15431c7c88367c751534d95a9e2002c8572f2776a278e43d", + "nonce": 0, + "round": 29791109, + "epoch": 2068, + "value": "0", + "receiver": "erd1qqqqqqqqqqqqqpgq4eehfw7kfnc8x9cf9nfejgmtknuz6kygx7tsvhn3uc", + "sender": "erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67", + "gasPrice": 1000000000, + "gasLimit": 24556500, + "data": "ZXhlY3V0ZUFyYml0cmFnZUA3ZmQzYmRkMjAyNDI3YzAwQDU4NDU0NzRjNDQyZDY1MzQzMTMzNjU2NA==", + "previousTransactionHash": "dd59dbfb06682d18a8a8571dd7039d388ee0f44ba6438022426ba4f0a6abf319", + "originalTransactionHash": "dd59dbfb06682d18a8a8571dd7039d388ee0f44ba6438022426ba4f0a6abf319", + "originalSender": "erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67", + "sourceShard": 1, + "destinationShard": 1, + "miniblockType": "SmartContractResultBlock", + "miniblockHash": "9ffbaf706ac92b240eecc2934a8f7449093a5472db6b8010dfb48821028557f1", + "status": "success", + "operation": "transfer", + "function": "executeArbitrage", + "callType": "directCall", + "options": 0 + }, + { + "type": "unsigned", + "processingTypeOnSource": "BuiltInFunctionCall", + "processingTypeOnDestination": "SCInvoking", + "hash": "751e080418ec1284a0372883c97f0a6f20f422ddbf69b52db9e75930dc03fdf4", + "nonce": 0, + "round": 29791109, + "epoch": 2068, + "value": "0", + "receiver": "erd1qqqqqqqqqqqqqpgqdj3p7vmlmf5r0rz4f37y7s9482gqagv32jpsj8g69g", + "sender": "erd1qqqqqqqqqqqqqpgq4eehfw7kfnc8x9cf9nfejgmtknuz6kygx7tsvhn3uc", + "gasPrice": 1000000000, + "data": "RVNEVFRyYW5zZmVyQDU3NDU0NzRjNDQyZDYyNjQzNDY0MzczOUA4YWM3MjMwNDg5ZTgwMDAwQDczNzc2MTcwNTQ2ZjZiNjU2ZTczNDY2OTc4NjU2NDQ5NmU3MDc1NzRANTg0NTQ3NGM0NDJkNjUzNDMxMzM2NTY0QDdmZDNiZGQyMDI0MjdjMDBAMzZjOGRhZTk0Yzc0NjNmYzk4ZjRhNTYxMzExNDBlZDI1ODFkNWJiMjcwNzgwNGQ2MGNkZGVkODE1YjhjYjUyM0BkZDU5ZGJmYjA2NjgyZDE4YThhODU3MWRkNzAzOWQzODhlZTBmNDRiYTY0MzgwMjI0MjZiYTRmMGE2YWJmMzE5", + "previousTransactionHash": "dd59dbfb06682d18a8a8571dd7039d388ee0f44ba6438022426ba4f0a6abf319", + "originalTransactionHash": "dd59dbfb06682d18a8a8571dd7039d388ee0f44ba6438022426ba4f0a6abf319", + "originalSender": "erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67", + "sourceShard": 1, + "destinationShard": 1, + "miniblockType": "SmartContractResultBlock", + "miniblockHash": "9ffbaf706ac92b240eecc2934a8f7449093a5472db6b8010dfb48821028557f1", + "status": "success", + "tokens": [ + "WEGLD-bd4d79" + ], + "esdtValues": [ + "10000000000000000000" + ], + "operation": "ESDTTransfer", + "function": "swapTokensFixedInput", + "callType": "asynchronousCall", + "options": 0 + }, + { + "type": "unsigned", + "processingTypeOnSource": "MoveBalance", + "processingTypeOnDestination": "MoveBalance", + "hash": "ed0412c3ae465216c50d60c30fb37cdfa61520ff773193fee457405f68c4a07a", + "nonce": 1052, + "round": 29791109, + "epoch": 2068, + "value": "29655420000000", + "receiver": "erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67", + "sender": "erd1qqqqqqqqqqqqqpgq4eehfw7kfnc8x9cf9nfejgmtknuz6kygx7tsvhn3uc", + "gasPrice": 1000000000, + "data": "QDZmNmI=", + "previousTransactionHash": "dd59dbfb06682d18a8a8571dd7039d388ee0f44ba6438022426ba4f0a6abf319", + "originalTransactionHash": "dd59dbfb06682d18a8a8571dd7039d388ee0f44ba6438022426ba4f0a6abf319", + "sourceShard": 1, + "destinationShard": 1, + "miniblockType": "SmartContractResultBlock", + "miniblockHash": "9ffbaf706ac92b240eecc2934a8f7449093a5472db6b8010dfb48821028557f1", + "status": "success", + "operation": "transfer", + "isRefund": true, + "callType": "directCall", + "options": 0 + } + ], + "indexOfFirstTxProcessed": 0, + "indexOfLastTxProcessed": 0 + } + ] + } +] diff --git a/server/services/transactionEvents.go b/server/services/transactionEvents.go index 562c2f4..7c7144d 100644 --- a/server/services/transactionEvents.go +++ b/server/services/transactionEvents.go @@ -6,9 +6,10 @@ import ( ) type eventTransferValueOnly struct { - sender string - receiver string - value string + sender string + receiver string + value string + isAsyncCallbackWithError bool } type eventESDT struct { @@ -18,6 +19,7 @@ type eventESDT struct { identifier string nonceAsBytes []byte value string + isAsyncCall bool } // newEventESDTFromBasicTopics creates an eventESDT from the given topics. The following topics are expected: diff --git a/server/services/transactionEventsController.go b/server/services/transactionEventsController.go index 20c7981..0bcf3e1 100644 --- a/server/services/transactionEventsController.go +++ b/server/services/transactionEventsController.go @@ -45,11 +45,22 @@ func (controller *transactionEventsController) extractEventSCDeploy(tx *transact } func (controller *transactionEventsController) extractEventTransferValueOnly(tx *transaction.ApiTransactionResult) ([]*eventTransferValueOnly, error) { + return controller.extractEventTransferValueWithDecideFunction(tx, controller.decideEffectiveEventTransferValueOnlyAfterSirius) +} + +func (controller *transactionEventsController) extractEventTransferValueWithAsyncCallbackUserError(tx *transaction.ApiTransactionResult) ([]*eventTransferValueOnly, error) { + return controller.extractEventTransferValueWithDecideFunction(tx, controller.detectEventTransferValueWithAsyncCallbackAndUserError) +} + +func (controller *transactionEventsController) extractEventTransferValueWithDecideFunction( + tx *transaction.ApiTransactionResult, + decide func(event *transaction.Events) (*eventTransferValueOnly, error), +) ([]*eventTransferValueOnly, error) { rawEvents := controller.findManyEventsByIdentifier(tx, transactionEventTransferValueOnly) typedEvents := make([]*eventTransferValueOnly, 0) for _, event := range rawEvents { - typedEvent, err := controller.decideEffectiveEventTransferValueOnlyAfterSirius(event) + typedEvent, err := decide(event) if err != nil { return nil, err } @@ -62,6 +73,48 @@ func (controller *transactionEventsController) extractEventTransferValueOnly(tx return typedEvents, nil } +func (controller *transactionEventsController) detectEventTransferValueWithAsyncCallbackAndUserError(event *transaction.Events) (*eventTransferValueOnly, error) { + numTopics := len(event.Topics) + if numTopics != numTopicsOfEventTransferValueOnlyAfterSirius { + return nil, fmt.Errorf("%w: bad number of topics for 'transferValueOnly' = %d", errCannotRecognizeEvent, numTopics) + } + + receiverPubKey := event.Topics[1] + eventData := string(event.Data) + if eventData != transactionEventDataAsyncCallback { + // not of interest, since is not an AsyncCallback + return nil, nil + } + + numElementsAdditionalData := len(event.AdditionalData) + if numElementsAdditionalData != numElementsInAdditionalDataAsyncCallbackWithError { + return nil, nil + } + + userErrorCode := int(new(big.Int).SetBytes(event.AdditionalData[2]).Int64()) + if userErrorCode != errorCodeUserError { + return nil, nil + } + + sender := event.Address + senderPubKey, err := controller.provider.ConvertAddressToPubKey(sender) + if err != nil { + return nil, err + } + isIntraShard := controller.provider.ComputeShardIdOfPubKey(senderPubKey) == controller.provider.ComputeShardIdOfPubKey(receiverPubKey) + if !isIntraShard { + // Ineffective event, the issue with this type of event is intra shard + return nil, nil + } + + receiver := controller.provider.ConvertPubKeyToAddress(receiverPubKey) + return &eventTransferValueOnly{ + sender: sender, + receiver: receiver, + isAsyncCallbackWithError: true, + }, nil +} + // See: https://github.com/multiversx/mx-specs/blob/main/releases/protocol/release-specs-v1.6.0-Sirius.md#17-logs--events-changes-5490 func (controller *transactionEventsController) decideEffectiveEventTransferValueOnlyAfterSirius(event *transaction.Events) (*eventTransferValueOnly, error) { numTopics := len(event.Topics) @@ -89,8 +142,8 @@ func (controller *transactionEventsController) decideEffectiveEventTransferValue return nil, err } - isIntrashard := controller.provider.ComputeShardIdOfPubKey(senderPubKey) == controller.provider.ComputeShardIdOfPubKey(receiverPubKey) - if !isIntrashard { + isIntraShard := controller.provider.ComputeShardIdOfPubKey(senderPubKey) == controller.provider.ComputeShardIdOfPubKey(receiverPubKey) + if !isIntraShard { // Ineffective event, since the balance change is already captured by a SCR. return nil, nil } @@ -157,6 +210,10 @@ func (controller *transactionEventsController) extractEventsESDTOrESDTNFTTransfe return nil, err } + if string(event.Data) == transactionEventDataAsyncCall { + typedEvent.isAsyncCall = true + } + receiverPubkey := event.Topics[3] typedEvent.receiverAddress = controller.provider.ConvertPubKeyToAddress(receiverPubkey) typedEvent.senderAddress = event.Address diff --git a/server/services/transactionEventsController_test.go b/server/services/transactionEventsController_test.go index 1f7067d..e65a8cd 100644 --- a/server/services/transactionEventsController_test.go +++ b/server/services/transactionEventsController_test.go @@ -314,6 +314,100 @@ func TestTransactionEventsController_ExtractEvents(t *testing.T) { require.Len(t, events, 0) }) + t.Run("transferValueOnly with AsyncCallback and user error", func(t *testing.T) { + topic1 := testscommon.TestContractBarShard0.PubKey + + tx := &transaction.ApiTransactionResult{ + Epoch: 43, + Logs: &transaction.ApiLogs{ + Events: []*transaction.Events{ + { + Identifier: "transferValueOnly", + Address: testscommon.TestContractFooShard0.Address, + Topics: [][]byte{ + nil, + topic1, + }, + Data: []byte("AsyncCallback"), + AdditionalData: [][]byte{ + []byte("AsyncCallback"), + nil, + {0x04}, + nil, + }, + }, + }, + }, + } + + events, err := controller.extractEventTransferValueWithAsyncCallbackUserError(tx) + require.NoError(t, err) + require.Len(t, events, 1) + require.Equal(t, &eventTransferValueOnly{ + sender: "erd1qqqqqqqqqqqqqpgqagjekf5mxv86hy5c62vvtug5vc6jmgcsq6uq8reras", + receiver: "erd1qqqqqqqqqqqqqpgqdstpe4fepzl4w8683xw88t5kcjkxz0zaq6uquj6ztu", + value: "", + isAsyncCallbackWithError: true, + }, events[0]) + }) + + t.Run("transferValueOnly with AsyncCallback no user error should be ignored", func(t *testing.T) { + topic1 := testscommon.TestContractBarShard0.PubKey + tx := &transaction.ApiTransactionResult{ + Epoch: 43, + Logs: &transaction.ApiLogs{ + Events: []*transaction.Events{ + { + Identifier: "transferValueOnly", + Address: testscommon.TestContractFooShard0.Address, + Topics: [][]byte{ + nil, + topic1, + }, + Data: []byte("AsyncCallback"), + AdditionalData: [][]byte{ + []byte("AsyncCallback"), + nil, + nil, + nil, + }, + }, + }, + }, + } + + events, err := controller.extractEventTransferValueWithAsyncCallbackUserError(tx) + require.NoError(t, err) + require.Len(t, events, 0) + }) + t.Run("transferValueOnly with AsyncCallback incorrect num of additional data", func(t *testing.T) { + topic1 := testscommon.TestContractBarShard0.PubKey + tx := &transaction.ApiTransactionResult{ + Epoch: 43, + Logs: &transaction.ApiLogs{ + Events: []*transaction.Events{ + { + Identifier: "transferValueOnly", + Address: testscommon.TestContractFooShard0.Address, + Topics: [][]byte{ + nil, + topic1, + }, + Data: []byte("AsyncCallback"), + AdditionalData: [][]byte{ + []byte("AsyncCallback"), + nil, + }, + }, + }, + }, + } + + events, err := controller.extractEventTransferValueWithAsyncCallbackUserError(tx) + require.NoError(t, err) + require.Len(t, events, 0) + }) + t.Run("ESDTNFTCreate", func(t *testing.T) { tx := &transaction.ApiTransactionResult{ Logs: &transaction.ApiLogs{ diff --git a/server/services/transactionsFeaturesDetector.go b/server/services/transactionsFeaturesDetector.go index cfde671..c365155 100644 --- a/server/services/transactionsFeaturesDetector.go +++ b/server/services/transactionsFeaturesDetector.go @@ -102,3 +102,23 @@ func (detector *transactionsFeaturesDetector) isIntrashard(tx *transaction.ApiTr func (detector *transactionsFeaturesDetector) isSmartContractResultIneffectiveRefund(scr *transaction.ApiTransactionResult) bool { return scr.IsRefund && scr.Sender == scr.Receiver && detector.networkProviderExtension.isContractAddress(scr.Sender) } + +func (detector *transactionsFeaturesDetector) isEventWithAsyncCallAndHasAnAsyncCallBackWithError(currentEvent *eventESDT, transferValueEvents []*eventTransferValueOnly) bool { + if !currentEvent.isAsyncCall { + return false + } + + for _, eventWithError := range transferValueEvents { + if !eventWithError.isAsyncCallbackWithError { + continue + } + + haveSenderAndReceiverInMirror := currentEvent.senderAddress == eventWithError.receiver && + currentEvent.receiverAddress == eventWithError.sender + if haveSenderAndReceiverInMirror { + return true + } + } + + return false +} diff --git a/server/services/transactionsFeaturesDetector_test.go b/server/services/transactionsFeaturesDetector_test.go index 6d69b6a..375a5f9 100644 --- a/server/services/transactionsFeaturesDetector_test.go +++ b/server/services/transactionsFeaturesDetector_test.go @@ -201,3 +201,57 @@ func TestTransactionsFeaturesDetector_isSmartContractResultIneffectiveRefund(t * IsRefund: false, })) } + +func TestTransactionsFeaturesDetector_isEventWithAsyncCallAndHasAnAsyncCallBackWithError(t *testing.T) { + networkProvider := testscommon.NewNetworkProviderMock() + detector := newTransactionsFeaturesDetector(networkProvider) + + t.Run("event is not an async call should return false", func(t *testing.T) { + require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(&eventESDT{}, nil)) + }) + + t.Run("no transfer value events should return false", func(t *testing.T) { + require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(&eventESDT{isAsyncCall: true}, nil)) + }) + + t.Run("no transfer value events with async callback", func(t *testing.T) { + events := []*eventTransferValueOnly{ + { + isAsyncCallbackWithError: false, + }, + } + require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(&eventESDT{isAsyncCall: true}, events)) + }) + + t.Run("sender and receiver are not in mirror should return false", func(t *testing.T) { + esdt := &eventESDT{ + isAsyncCall: true, + senderAddress: "sender", + receiverAddress: "receiver", + } + events := []*eventTransferValueOnly{ + { + isAsyncCallbackWithError: true, + sender: "sender", + receiver: "receiver", + }, + } + require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(esdt, events)) + }) + + t.Run("async callback with error and correct sender and receiver should return true", func(t *testing.T) { + esdt := &eventESDT{ + isAsyncCall: true, + senderAddress: "sender", + receiverAddress: "receiver", + } + events := []*eventTransferValueOnly{ + { + isAsyncCallbackWithError: true, + sender: "receiver", + receiver: "sender", + }, + } + require.True(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(esdt, events)) + }) +} diff --git a/server/services/transactionsTransformer.go b/server/services/transactionsTransformer.go index bae4a71..aef95db 100644 --- a/server/services/transactionsTransformer.go +++ b/server/services/transactionsTransformer.go @@ -343,6 +343,11 @@ func (transformer *transactionsTransformer) addOperationsGivenTransactionEvents( return err } + eventsTransferValueIdentifierAsyncCallbackUserError, err := transformer.eventsController.extractEventTransferValueWithAsyncCallbackUserError(tx) + if err != nil { + return err + } + eventsESDTTransfer, err := transformer.eventsController.extractEventsESDTOrESDTNFTTransfers(tx) if err != nil { return err @@ -425,6 +430,11 @@ func (transformer *transactionsTransformer) addOperationsGivenTransactionEvents( } for _, event := range eventsESDTTransfer { + shouldIgnore := transformer.featuresDetector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(event, eventsTransferValueIdentifierAsyncCallbackUserError) + if shouldIgnore { + continue + } + operations := transformer.extractOperationsFromEventESDT(event) rosettaTx.Operations = append(rosettaTx.Operations, operations...) } diff --git a/server/services/transactionsTransformer_test.go b/server/services/transactionsTransformer_test.go index ef5f033..f9faa0e 100644 --- a/server/services/transactionsTransformer_test.go +++ b/server/services/transactionsTransformer_test.go @@ -663,6 +663,66 @@ func TestTransactionsTransformer_ExtractOperationsFromEventESDT(t *testing.T) { }) } +func TestTransactionsTransformer_TransformBlockTxsHavingESDTTransferAndLogs(t *testing.T) { + networkProvider := testscommon.NewNetworkProviderMock() + networkProvider.MockCustomCurrencies = []resources.Currency{{Symbol: "WEGLD-bd4d79"}} + networkProvider.MockObservedActualShard = 1 + + extension := newNetworkProviderExtension(networkProvider) + transformer := newTransactionsTransformer(networkProvider) + + blocks, err := readTestBlocks("testdata/blocks_with_esdt_transfer_and_error.json") + require.Nil(t, err) + + txs, err := transformer.transformBlockTxs(blocks[0]) + require.Nil(t, err) + require.Len(t, txs, 2) + + expectedTransferTx := &types.Transaction{ + TransactionIdentifier: hashToTransactionIdentifier("dd59dbfb06682d18a8a8571dd7039d388ee0f44ba6438022426ba4f0a6abf319"), + Operations: []*types.Operation{ + { + Type: opFee, + OperationIdentifier: indexToOperationIdentifier(0), + Account: addressToAccountIdentifier("erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67"), + Amount: extension.valueToNativeAmount("-491065000000000"), + Status: &opStatusSuccess, + }, + { + Type: opCustomTransfer, + OperationIdentifier: indexToOperationIdentifier(1), + Account: addressToAccountIdentifier("erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67"), + Amount: extension.valueToCustomAmount("-10000000000000000000", "WEGLD-bd4d79"), + Status: &opStatusSuccess, + }, + { + Type: opCustomTransfer, + OperationIdentifier: indexToOperationIdentifier(2), + Account: addressToAccountIdentifier("erd1qqqqqqqqqqqqqpgq4eehfw7kfnc8x9cf9nfejgmtknuz6kygx7tsvhn3uc"), + Amount: extension.valueToCustomAmount("10000000000000000000", "WEGLD-bd4d79"), + Status: &opStatusSuccess, + }, + }, + Metadata: extractTransactionMetadata(blocks[0].MiniBlocks[0].Transactions[0]), + } + + expectedRefundTx := &types.Transaction{ + TransactionIdentifier: hashToTransactionIdentifier("ed0412c3ae465216c50d60c30fb37cdfa61520ff773193fee457405f68c4a07a"), + Operations: []*types.Operation{ + { + Type: opFeeRefundAsScResult, + OperationIdentifier: indexToOperationIdentifier(0), + Account: addressToAccountIdentifier("erd1g4sw9ylunfgtj03vt8kvjc0kxg8m6cue32pcwh6nsyvqqnlkx7ts272l67"), + Amount: extension.valueToNativeAmount("29655420000000"), + Status: &opStatusSuccess, + }, + }, + } + + require.Equal(t, expectedTransferTx, txs[0]) + require.Equal(t, expectedRefundTx, txs[1]) +} + func TestTransactionsTransformer_TransformBlockTxsHavingESDTTransfer(t *testing.T) { networkProvider := testscommon.NewNetworkProviderMock() networkProvider.MockCustomCurrencies = []resources.Currency{{Symbol: "ROSETTA-3a2edf"}}