From 23f7a8164b7ac0e85b590bc27df409429d7d3163 Mon Sep 17 00:00:00 2001 From: miiu Date: Wed, 1 Apr 2026 14:43:46 +0300 Subject: [PATCH 1/3] fixes --- server/services/constants.go | 7 + .../blocks_with_esdt_transfer_and_error.json | 229 ++++++++++++++++++ server/services/transactionEvents.go | 8 +- .../services/transactionEventsController.go | 63 ++++- .../transactionEventsController_test.go | 94 +++++++ .../services/transactionsFeaturesDetector.go | 20 ++ .../transactionsFeaturesDetector_test.go | 54 +++++ server/services/transactionsTransformer.go | 10 + .../services/transactionsTransformer_test.go | 60 +++++ 9 files changed, 539 insertions(+), 6 deletions(-) create mode 100644 server/services/testdata/blocks_with_esdt_transfer_and_error.json diff --git a/server/services/constants.go b/server/services/constants.go index 0f890566..0e3fab5c 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 00000000..dccd5029 --- /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 562c2f4b..bd1e90ec 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 + asyncCallbackWithError bool } type eventESDT struct { @@ -18,6 +19,7 @@ type eventESDT struct { identifier string nonceAsBytes []byte value string + asyncCall 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 20c79811..f6d92cd6 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.decideEffectiveEventTransferValueWithAsyncCallbackAndUserError) +} + +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) decideEffectiveEventTransferValueWithAsyncCallbackAndUserError(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 { + // Ineffective event, 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, + asyncCallbackWithError: 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.asyncCall = 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 1f7067d9..38bda384 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: "", + asyncCallbackWithError: 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 cfde671f..720ad02f 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.asyncCall { + return false + } + + for _, eventWithError := range transferValueEvents { + if !eventWithError.asyncCallbackWithError { + return false + } + + 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 6d69b6a8..48027645 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{asyncCall: true}, nil)) + }) + + t.Run("no transfer value events with async callback", func(t *testing.T) { + events := []*eventTransferValueOnly{ + { + asyncCallbackWithError: false, + }, + } + require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(&eventESDT{asyncCall: true}, events)) + }) + + t.Run("sender and receiver are not in mirror should return false", func(t *testing.T) { + esdt := &eventESDT{ + asyncCall: true, + senderAddress: "sender", + receiverAddress: "receiver", + } + events := []*eventTransferValueOnly{ + { + asyncCallbackWithError: 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{ + asyncCall: true, + senderAddress: "sender", + receiverAddress: "receiver", + } + events := []*eventTransferValueOnly{ + { + asyncCallbackWithError: 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 bae4a71b..aef95dbf 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 ef5f0333..f9faa0e8 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"}} From df90b5aabae87e88e1ae8a5a049265074fcf55d5 Mon Sep 17 00:00:00 2001 From: miiu Date: Wed, 1 Apr 2026 15:02:51 +0300 Subject: [PATCH 2/3] fixes after review --- server/services/transactionsFeaturesDetector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/transactionsFeaturesDetector.go b/server/services/transactionsFeaturesDetector.go index 720ad02f..7910cd06 100644 --- a/server/services/transactionsFeaturesDetector.go +++ b/server/services/transactionsFeaturesDetector.go @@ -110,7 +110,7 @@ func (detector *transactionsFeaturesDetector) isEventWithAsyncCallAndHasAnAsyncC for _, eventWithError := range transferValueEvents { if !eventWithError.asyncCallbackWithError { - return false + continue } haveSenderAndReceiverInMirror := currentEvent.senderAddress == eventWithError.receiver && From 30d0280789a4027c361b137bb2e8fc1c3d5f2310 Mon Sep 17 00:00:00 2001 From: miiu Date: Thu, 2 Apr 2026 11:49:56 +0300 Subject: [PATCH 3/3] fixes after first review --- server/services/transactionEvents.go | 10 ++++----- .../services/transactionEventsController.go | 14 ++++++------ .../transactionEventsController_test.go | 8 +++---- .../services/transactionsFeaturesDetector.go | 4 ++-- .../transactionsFeaturesDetector_test.go | 22 +++++++++---------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/server/services/transactionEvents.go b/server/services/transactionEvents.go index bd1e90ec..7c7144d4 100644 --- a/server/services/transactionEvents.go +++ b/server/services/transactionEvents.go @@ -6,10 +6,10 @@ import ( ) type eventTransferValueOnly struct { - sender string - receiver string - value string - asyncCallbackWithError bool + sender string + receiver string + value string + isAsyncCallbackWithError bool } type eventESDT struct { @@ -19,7 +19,7 @@ type eventESDT struct { identifier string nonceAsBytes []byte value string - asyncCall bool + 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 f6d92cd6..0bcf3e18 100644 --- a/server/services/transactionEventsController.go +++ b/server/services/transactionEventsController.go @@ -49,7 +49,7 @@ func (controller *transactionEventsController) extractEventTransferValueOnly(tx } func (controller *transactionEventsController) extractEventTransferValueWithAsyncCallbackUserError(tx *transaction.ApiTransactionResult) ([]*eventTransferValueOnly, error) { - return controller.extractEventTransferValueWithDecideFunction(tx, controller.decideEffectiveEventTransferValueWithAsyncCallbackAndUserError) + return controller.extractEventTransferValueWithDecideFunction(tx, controller.detectEventTransferValueWithAsyncCallbackAndUserError) } func (controller *transactionEventsController) extractEventTransferValueWithDecideFunction( @@ -73,7 +73,7 @@ func (controller *transactionEventsController) extractEventTransferValueWithDeci return typedEvents, nil } -func (controller *transactionEventsController) decideEffectiveEventTransferValueWithAsyncCallbackAndUserError(event *transaction.Events) (*eventTransferValueOnly, error) { +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) @@ -82,7 +82,7 @@ func (controller *transactionEventsController) decideEffectiveEventTransferValue receiverPubKey := event.Topics[1] eventData := string(event.Data) if eventData != transactionEventDataAsyncCallback { - // Ineffective event, since is not an AsyncCallback + // not of interest, since is not an AsyncCallback return nil, nil } @@ -109,9 +109,9 @@ func (controller *transactionEventsController) decideEffectiveEventTransferValue receiver := controller.provider.ConvertPubKeyToAddress(receiverPubKey) return &eventTransferValueOnly{ - sender: sender, - receiver: receiver, - asyncCallbackWithError: true, + sender: sender, + receiver: receiver, + isAsyncCallbackWithError: true, }, nil } @@ -211,7 +211,7 @@ func (controller *transactionEventsController) extractEventsESDTOrESDTNFTTransfe } if string(event.Data) == transactionEventDataAsyncCall { - typedEvent.asyncCall = true + typedEvent.isAsyncCall = true } receiverPubkey := event.Topics[3] diff --git a/server/services/transactionEventsController_test.go b/server/services/transactionEventsController_test.go index 38bda384..e65a8cd9 100644 --- a/server/services/transactionEventsController_test.go +++ b/server/services/transactionEventsController_test.go @@ -344,10 +344,10 @@ func TestTransactionEventsController_ExtractEvents(t *testing.T) { require.NoError(t, err) require.Len(t, events, 1) require.Equal(t, &eventTransferValueOnly{ - sender: "erd1qqqqqqqqqqqqqpgqagjekf5mxv86hy5c62vvtug5vc6jmgcsq6uq8reras", - receiver: "erd1qqqqqqqqqqqqqpgqdstpe4fepzl4w8683xw88t5kcjkxz0zaq6uquj6ztu", - value: "", - asyncCallbackWithError: true, + sender: "erd1qqqqqqqqqqqqqpgqagjekf5mxv86hy5c62vvtug5vc6jmgcsq6uq8reras", + receiver: "erd1qqqqqqqqqqqqqpgqdstpe4fepzl4w8683xw88t5kcjkxz0zaq6uquj6ztu", + value: "", + isAsyncCallbackWithError: true, }, events[0]) }) diff --git a/server/services/transactionsFeaturesDetector.go b/server/services/transactionsFeaturesDetector.go index 7910cd06..c365155d 100644 --- a/server/services/transactionsFeaturesDetector.go +++ b/server/services/transactionsFeaturesDetector.go @@ -104,12 +104,12 @@ func (detector *transactionsFeaturesDetector) isSmartContractResultIneffectiveRe } func (detector *transactionsFeaturesDetector) isEventWithAsyncCallAndHasAnAsyncCallBackWithError(currentEvent *eventESDT, transferValueEvents []*eventTransferValueOnly) bool { - if !currentEvent.asyncCall { + if !currentEvent.isAsyncCall { return false } for _, eventWithError := range transferValueEvents { - if !eventWithError.asyncCallbackWithError { + if !eventWithError.isAsyncCallbackWithError { continue } diff --git a/server/services/transactionsFeaturesDetector_test.go b/server/services/transactionsFeaturesDetector_test.go index 48027645..375a5f9e 100644 --- a/server/services/transactionsFeaturesDetector_test.go +++ b/server/services/transactionsFeaturesDetector_test.go @@ -211,29 +211,29 @@ func TestTransactionsFeaturesDetector_isEventWithAsyncCallAndHasAnAsyncCallBackW }) t.Run("no transfer value events should return false", func(t *testing.T) { - require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(&eventESDT{asyncCall: true}, nil)) + require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(&eventESDT{isAsyncCall: true}, nil)) }) t.Run("no transfer value events with async callback", func(t *testing.T) { events := []*eventTransferValueOnly{ { - asyncCallbackWithError: false, + isAsyncCallbackWithError: false, }, } - require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(&eventESDT{asyncCall: true}, events)) + 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{ - asyncCall: true, + isAsyncCall: true, senderAddress: "sender", receiverAddress: "receiver", } events := []*eventTransferValueOnly{ { - asyncCallbackWithError: true, - sender: "sender", - receiver: "receiver", + isAsyncCallbackWithError: true, + sender: "sender", + receiver: "receiver", }, } require.False(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(esdt, events)) @@ -241,15 +241,15 @@ func TestTransactionsFeaturesDetector_isEventWithAsyncCallAndHasAnAsyncCallBackW t.Run("async callback with error and correct sender and receiver should return true", func(t *testing.T) { esdt := &eventESDT{ - asyncCall: true, + isAsyncCall: true, senderAddress: "sender", receiverAddress: "receiver", } events := []*eventTransferValueOnly{ { - asyncCallbackWithError: true, - sender: "receiver", - receiver: "sender", + isAsyncCallbackWithError: true, + sender: "receiver", + receiver: "sender", }, } require.True(t, detector.isEventWithAsyncCallAndHasAnAsyncCallBackWithError(esdt, events))