diff --git a/bridges/ethMultiversX/bridgeExecutor.go b/bridges/ethMultiversX/bridgeExecutor.go index 52bf6483..3d1529f7 100644 --- a/bridges/ethMultiversX/bridgeExecutor.go +++ b/bridges/ethMultiversX/bridgeExecutor.go @@ -14,14 +14,30 @@ import ( "github.com/multiversx/mx-bridge-eth-go/core" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" "github.com/multiversx/mx-bridge-eth-go/core/batchProcessor" + "github.com/multiversx/mx-bridge-eth-go/core/converters" "github.com/multiversx/mx-chain-core-go/core/check" + "github.com/multiversx/mx-chain-core-go/data/transaction" logger "github.com/multiversx/mx-chain-logger-go" ) // splits - represent the number of times we split the maximum interval // we wait for the transfer confirmation on Ethereum -const splits = 10 -const minRetries = 1 + +const ( + // splits - represent the number of times we split the maximum interval + // we wait for the transfer confirmation on Ethereum + splits = 10 + + // Minimum number of retries + minRetries = 1 + + // Number of topics required for createTransactionScCallEvent + scCallEventTopicsCount = 9 + + // Indices for specific topics in createTransactionScCallEvent + depositNonceIndex = 1 + calldataIndex = 8 +) // ArgsBridgeExecutor is the arguments DTO struct used in both bridges type ArgsBridgeExecutor struct { @@ -153,10 +169,28 @@ func (executor *bridgeExecutor) MyTurnAsLeader() bool { // GetBatchFromMultiversX fetches the pending batch from MultiversX func (executor *bridgeExecutor) GetBatchFromMultiversX(ctx context.Context) (*bridgeCore.TransferBatch, error) { batch, err := executor.multiversXClient.GetPendingBatch(ctx) - if err == nil { - executor.statusHandler.SetIntMetric(core.MetricNumBatches, int(batch.ID)-1) + if err != nil { + return nil, err + } + + if batch == nil { + return nil, ErrNilBatch + } + + executor.statusHandler.SetIntMetric(core.MetricNumBatches, int(batch.ID)-1) + + isBatchInvalid := len(batch.Deposits) == 0 + if isBatchInvalid { + return nil, fmt.Errorf("%w, fetched nonce: %d", + ErrBatchWithoutDeposits, batch.ID) + } + + batch, err = executor.addBatchSCMetadataMvx(ctx, batch) + if err != nil { + return nil, err } - return batch, err + + return batch, nil } // StoreBatchFromMultiversX saves the pending batch from MultiversX @@ -169,6 +203,53 @@ func (executor *bridgeExecutor) StoreBatchFromMultiversX(batch *bridgeCore.Trans return nil } +// addBatchSCMetadataMvx fetches the logs containing sc calls metadata for the current batch +func (executor *bridgeExecutor) addBatchSCMetadataMvx(ctx context.Context, batch *bridgeCore.TransferBatch) (*bridgeCore.TransferBatch, error) { + events, err := executor.multiversXClient.GetBatchSCMetadata(ctx, batch) + if err != nil { + return nil, err + } + + eventsByDepositNonce := make(map[uint64]*transaction.Events) + + for _, event := range events { + if len(event.Topics) < scCallEventTopicsCount { + return nil, ErrInvalidTopicsNumber + } + + depositNonceBytes := event.Topics[depositNonceIndex] + depositNonce, err := converters.ParseUInt64FromByteSlice(depositNonceBytes) + if err != nil { + return nil, fmt.Errorf("%w while parsing deposit nonce", err) + } + + eventsByDepositNonce[depositNonce] = event + } + + for _, t := range batch.Deposits { + executor.addMetadataToTransferMvx(t, eventsByDepositNonce) + } + + return batch, nil +} + +// addMetadataToTransferMvx fetches the logs containing sc calls metadata for the current batch +func (executor *bridgeExecutor) addMetadataToTransferMvx(transfer *bridgeCore.DepositTransfer, eventsByDepositNonce map[uint64]*transaction.Events) { + event, exists := eventsByDepositNonce[transfer.Nonce] + if !exists { + transfer.DisplayableData = "" + return + } + + calldataBytes := event.Topics[calldataIndex] + processDataForMvx(transfer, calldataBytes) +} + +func processDataForMvx(transfer *bridgeCore.DepositTransfer, buff []byte) { + transfer.Data = buff + transfer.DisplayableData = hex.EncodeToString(transfer.Data) +} + // GetStoredBatch returns the stored batch func (executor *bridgeExecutor) GetStoredBatch() *bridgeCore.TransferBatch { return executor.batch @@ -462,38 +543,41 @@ func (executor *bridgeExecutor) GetAndStoreBatchFromEthereum(ctx context.Context } // addBatchSCMetadata fetches the logs containing sc calls metadata for the current batch -func (executor *bridgeExecutor) addBatchSCMetadata(ctx context.Context, transfers *bridgeCore.TransferBatch) (*bridgeCore.TransferBatch, error) { - if transfers == nil { +func (executor *bridgeExecutor) addBatchSCMetadata(ctx context.Context, batch *bridgeCore.TransferBatch) (*bridgeCore.TransferBatch, error) { + if batch == nil { return nil, ErrNilBatch } - events, err := executor.ethereumClient.GetBatchSCMetadata(ctx, transfers.ID, int64(transfers.BlockNumber)) + events, err := executor.ethereumClient.GetBatchSCMetadata(ctx, batch.ID, int64(batch.BlockNumber)) if err != nil { return nil, err } - for i, t := range transfers.Deposits { - transfers.Deposits[i] = executor.addMetadataToTransfer(t, events) + eventsByDepositNonce := make(map[uint64]*contract.ERC20SafeERC20SCDeposit) + + for _, event := range events { + eventsByDepositNonce[event.DepositNonce.Uint64()] = event + } + + for _, t := range batch.Deposits { + executor.addMetadataToTransfer(t, eventsByDepositNonce) } - return transfers, nil + return batch, nil } -func (executor *bridgeExecutor) addMetadataToTransfer(transfer *bridgeCore.DepositTransfer, events []*contract.ERC20SafeERC20SCDeposit) *bridgeCore.DepositTransfer { - for _, event := range events { - if event.DepositNonce.Uint64() == transfer.Nonce { - processData(transfer, event.CallData) - return transfer - } +func (executor *bridgeExecutor) addMetadataToTransfer(transfer *bridgeCore.DepositTransfer, eventsByDepositNonce map[uint64]*contract.ERC20SafeERC20SCDeposit) { + event, exist := eventsByDepositNonce[transfer.Nonce] + if !exist { + transfer.Data = []byte{bridgeCore.MissingDataProtocolMarker} + transfer.DisplayableData = "" + return } - transfer.Data = []byte{bridgeCore.MissingDataProtocolMarker} - transfer.DisplayableData = "" - - return transfer + processDataForEth(transfer, event.CallData) } -func processData(transfer *bridgeCore.DepositTransfer, buff []byte) { +func processDataForEth(transfer *bridgeCore.DepositTransfer, buff []byte) { transfer.Data = buff dataLen := len(transfer.Data) if dataLen == 0 { diff --git a/bridges/ethMultiversX/bridgeExecutor_test.go b/bridges/ethMultiversX/bridgeExecutor_test.go index 2751ae31..4ea9f639 100644 --- a/bridges/ethMultiversX/bridgeExecutor_test.go +++ b/bridges/ethMultiversX/bridgeExecutor_test.go @@ -2,6 +2,8 @@ package ethmultiversx import ( "context" + "encoding/binary" + "encoding/hex" "errors" "fmt" "math/big" @@ -17,6 +19,7 @@ import ( "github.com/multiversx/mx-bridge-eth-go/testsCommon" bridgeTests "github.com/multiversx/mx-bridge-eth-go/testsCommon/bridge" "github.com/multiversx/mx-chain-core-go/core/check" + "github.com/multiversx/mx-chain-core-go/data/transaction" logger "github.com/multiversx/mx-chain-logger-go" "github.com/stretchr/testify/assert" ) @@ -991,27 +994,100 @@ func TestMultiversXToEthBridgeExecutor_GetAndStoreBatchFromMultiversX(t *testing err := executor.StoreBatchFromMultiversX(nil) assert.Equal(t, ErrNilBatch, err) }) - t.Run("should work", func(t *testing.T) { + t.Run("no deposits should error", func(t *testing.T) { t.Parallel() - wasCalled := false args := createMockExecutorArgs() + wasCalled := false args.MultiversXClient = &bridgeTests.MultiversXClientStub{ GetPendingBatchCalled: func(ctx context.Context) (*bridgeCore.TransferBatch, error) { wasCalled = true - return providedBatch, nil + return &bridgeCore.TransferBatch{}, nil }, } executor, _ := NewBridgeExecutor(args) batch, err := executor.GetBatchFromMultiversX(context.Background()) assert.True(t, wasCalled) - assert.Equal(t, providedBatch, batch) + assert.Nil(t, batch) + assert.Equal(t, fmt.Errorf("%w, fetched nonce: %d", ErrBatchWithoutDeposits, 0), err) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + wasPendingBatchCalled := false + wasGetBatchSCMetadataCalled := false + providedBatchWithDeposits := &bridgeCore.TransferBatch{ + Deposits: []*bridgeCore.DepositTransfer{{}, {}}, + } + args := createMockExecutorArgs() + args.MultiversXClient = &bridgeTests.MultiversXClientStub{ + GetPendingBatchCalled: func(ctx context.Context) (*bridgeCore.TransferBatch, error) { + wasPendingBatchCalled = true + return providedBatchWithDeposits, nil + }, + GetBatchSCMetadataCalled: func(ctx context.Context, batch *bridgeCore.TransferBatch) ([]*transaction.Events, error) { + wasGetBatchSCMetadataCalled = true + return []*transaction.Events{}, nil + }, + } + + executor, _ := NewBridgeExecutor(args) + batch, err := executor.GetBatchFromMultiversX(context.Background()) + assert.True(t, wasPendingBatchCalled) + assert.True(t, wasGetBatchSCMetadataCalled) + assert.Equal(t, providedBatchWithDeposits, batch) assert.Nil(t, err) err = executor.StoreBatchFromMultiversX(batch) - assert.Equal(t, providedBatch, executor.batch) + assert.Equal(t, providedBatchWithDeposits, executor.batch) + assert.Nil(t, err) + }) + t.Run("should add deposits metadata for sc calls", func(t *testing.T) { + t.Parallel() + + providedNonce := uint64(8346) + depositNonce := uint64(100) + + providedBatchWithDeposit := &bridgeCore.TransferBatch{ + ID: providedNonce, + Deposits: []*bridgeCore.DepositTransfer{ + { + Nonce: depositNonce, + }, + }, + } + + depositData := []byte("testData") + expectedBatch := &bridgeCore.TransferBatch{ + ID: providedNonce, + Deposits: []*bridgeCore.DepositTransfer{ + { + Nonce: depositNonce, + Data: depositData, + DisplayableData: hex.EncodeToString(depositData), + }, + }, + } + + args := createMockExecutorArgs() + args.MultiversXClient = &bridgeTests.MultiversXClientStub{ + GetPendingBatchCalled: func(ctx context.Context) (*bridgeCore.TransferBatch, error) { + return providedBatchWithDeposit, nil + }, + GetBatchSCMetadataCalled: func(ctx context.Context, batch *bridgeCore.TransferBatch) ([]*transaction.Events, error) { + depositNonceBytes := make([]byte, 8) + binary.BigEndian.PutUint64(depositNonceBytes, depositNonce) + return []*transaction.Events{ + {Topics: [][]byte{{}, depositNonceBytes, {}, {}, {}, {}, {}, {}, depositData}}, + }, nil + }, + } + + executor, _ := NewBridgeExecutor(args) + batch, err := executor.GetBatchFromMultiversX(context.Background()) assert.Nil(t, err) + assert.Equal(t, expectedBatch, batch) }) } diff --git a/bridges/ethMultiversX/errors.go b/bridges/ethMultiversX/errors.go index 6986f858..3fce2d70 100644 --- a/bridges/ethMultiversX/errors.go +++ b/bridges/ethMultiversX/errors.go @@ -35,6 +35,12 @@ var ErrNilStatusHandler = errors.New("nil status handler") // ErrFinalBatchNotFound signals that a final batch was not found var ErrFinalBatchNotFound = errors.New("final batch not found") +// ErrBatchWithoutDeposits signals that a batch was found, but has no deposits +var ErrBatchWithoutDeposits = errors.New("batch was found, but has no deposits") + +// ErrInvalidTopicsNumber signals that an invalid number of topics was provided +var ErrInvalidTopicsNumber = errors.New("invalid number of topics") + // ErrNilSignaturesHolder signals that a nil signatures holder was provided var ErrNilSignaturesHolder = errors.New("nil signatures holder") diff --git a/bridges/ethMultiversX/interface.go b/bridges/ethMultiversX/interface.go index 9e459101..e6127dc2 100644 --- a/bridges/ethMultiversX/interface.go +++ b/bridges/ethMultiversX/interface.go @@ -8,6 +8,7 @@ import ( "github.com/multiversx/mx-bridge-eth-go/clients/ethereum/contract" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" "github.com/multiversx/mx-bridge-eth-go/core/batchProcessor" + "github.com/multiversx/mx-chain-core-go/data/transaction" ) // MultiversXClient defines the behavior of the MultiversX client able to communicate with the MultiversX chain @@ -25,6 +26,7 @@ type MultiversXClient interface { GetLastExecutedEthBatchID(ctx context.Context) (uint64, error) GetLastExecutedEthTxID(ctx context.Context) (uint64, error) GetCurrentNonce(ctx context.Context) (uint64, error) + GetBatchSCMetadata(ctx context.Context, batch *bridgeCore.TransferBatch) ([]*transaction.Events, error) ProposeSetStatus(ctx context.Context, batch *bridgeCore.TransferBatch) (string, error) ProposeTransfer(ctx context.Context, batch *bridgeCore.TransferBatch) (string, error) diff --git a/clients/multiversx/client.go b/clients/multiversx/client.go index b9fd0847..7464dd5d 100644 --- a/clients/multiversx/client.go +++ b/clients/multiversx/client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "math" "math/big" "reflect" "sync" @@ -13,8 +14,10 @@ import ( "github.com/multiversx/mx-bridge-eth-go/config" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" "github.com/multiversx/mx-bridge-eth-go/core/converters" + mxCore "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-core-go/data/api" + "github.com/multiversx/mx-chain-core-go/data/transaction" crypto "github.com/multiversx/mx-chain-crypto-go" "github.com/multiversx/mx-chain-crypto-go/signing/ed25519/singlesig" logger "github.com/multiversx/mx-chain-logger-go" @@ -47,6 +50,8 @@ type ClientArgs struct { RoleProvider roleProvider StatusHandler bridgeCore.StatusHandler ClientAvailabilityAllowDelta uint64 + EventsBlockRangeFrom int64 + EventsBlockRangeTo int64 } // client represents the MultiversX Client implementation @@ -63,6 +68,8 @@ type client struct { addressPublicKeyConverter bridgeCore.AddressConverter statusHandler bridgeCore.StatusHandler clientAvailabilityAllowDelta uint64 + eventsBlockRangeFrom int64 + eventsBlockRangeTo int64 lastNonce uint64 retriesAvailabilityCheck uint64 @@ -141,6 +148,8 @@ func NewClient(args ClientArgs) (*client, error) { tokensMapper: args.TokensMapper, statusHandler: args.StatusHandler, clientAvailabilityAllowDelta: args.ClientAvailabilityAllowDelta, + eventsBlockRangeFrom: args.EventsBlockRangeFrom, + eventsBlockRangeTo: args.EventsBlockRangeTo, } bech32RelayerAddress, _ := relayerAddress.AddressAsBech32String() @@ -236,6 +245,57 @@ func emptyResponse(response [][]byte) bool { return len(response) == 0 || (len(response) == 1 && len(response[0]) == 0) } +// GetBatchSCMetadata returns the emitted logs in a batch that hold metadata for SC execution on ETH +func (c *client) GetBatchSCMetadata(ctx context.Context, batch *bridgeCore.TransferBatch) ([]*transaction.Events, error) { + safeContractAddress, err := c.safeContractAddress.AddressAsBech32String() + if err != nil { + c.log.Error("error getting safe contract address", "error", err) + return nil, err + } + + eventInBytes := []byte("createTransactionScCallEvent") + + minBlockNumber := int64(math.MaxInt64) + maxBlockNumber := int64(math.MinInt64) + for _, dt := range batch.Deposits { + depositNonce := int64(dt.DepositBlockNumber) + + if depositNonce < minBlockNumber { + minBlockNumber = depositNonce + } + if depositNonce > maxBlockNumber { + maxBlockNumber = depositNonce + } + } + + // checks for underflow + fromBlock := minBlockNumber + c.eventsBlockRangeFrom + if fromBlock < 0 { + fromBlock = 0 + } + + toBlock := maxBlockNumber + c.eventsBlockRangeTo + if toBlock < 0 { + toBlock = 0 + } + + query := core.FilterQuery{ + Addresses: []string{safeContractAddress}, + Topics: [][]byte{eventInBytes}, + FromBlock: mxCore.OptionalUint64{Value: uint64(fromBlock), HasValue: true}, + ToBlock: mxCore.OptionalUint64{Value: uint64(toBlock), HasValue: true}, + } + + events, err := c.proxy.FilterLogs(ctx, &query) + if err != nil { + c.log.Error("error filtering logs", "error", err) + return nil, err + } + + c.log.Info(fmt.Sprintf("%+v", events)) + return events, nil +} + func (c *client) createPendingBatchFromResponse(ctx context.Context, responseData [][]byte) (*bridgeCore.TransferBatch, error) { numFieldsForTransaction := 6 dataLen := len(responseData) @@ -244,7 +304,7 @@ func (c *client) createPendingBatchFromResponse(ctx context.Context, responseDat return nil, fmt.Errorf("%w, got %d argument(s)", errInvalidNumberOfArguments, dataLen) } - batchID, err := parseUInt64FromByteSlice(responseData[0]) + batchID, err := converters.ParseUInt64FromByteSlice(responseData[0]) if err != nil { return nil, fmt.Errorf("%w while parsing batch ID", err) } @@ -257,21 +317,27 @@ func (c *client) createPendingBatchFromResponse(ctx context.Context, responseDat transferIndex := 0 for i := 1; i < dataLen; i += numFieldsForTransaction { // blockNonce is the i-th element, let's ignore it for now - depositNonce, errParse := parseUInt64FromByteSlice(responseData[i+1]) + blockNonce, errParse := converters.ParseUInt64FromByteSlice(responseData[i]) + if errParse != nil { + return nil, fmt.Errorf("%w while parsing the block nonce, transfer index %d", errParse, transferIndex) + } + + depositNonce, errParse := converters.ParseUInt64FromByteSlice(responseData[i+1]) if errParse != nil { return nil, fmt.Errorf("%w while parsing the deposit nonce, transfer index %d", errParse, transferIndex) } amount := big.NewInt(0).SetBytes(responseData[i+5]) deposit := &bridgeCore.DepositTransfer{ - Nonce: depositNonce, - FromBytes: responseData[i+2], - DisplayableFrom: c.addressPublicKeyConverter.ToBech32StringSilent(responseData[i+2]), - ToBytes: responseData[i+3], - DisplayableTo: c.addressPublicKeyConverter.ToHexStringWithPrefix(responseData[i+3]), - SourceTokenBytes: responseData[i+4], - DisplayableToken: string(responseData[i+4]), - Amount: amount, + DepositBlockNumber: blockNonce, + Nonce: depositNonce, + FromBytes: responseData[i+2], + DisplayableFrom: c.addressPublicKeyConverter.ToBech32StringSilent(responseData[i+2]), + ToBytes: responseData[i+3], + DisplayableTo: c.addressPublicKeyConverter.ToHexStringWithPrefix(responseData[i+3]), + SourceTokenBytes: responseData[i+4], + DisplayableToken: string(responseData[i+4]), + Amount: amount, } storedConvertedTokenBytes, exists := cachedTokens[deposit.DisplayableToken] diff --git a/clients/multiversx/client_test.go b/clients/multiversx/client_test.go index 9fe43810..6c1f1643 100644 --- a/clients/multiversx/client_test.go +++ b/clients/multiversx/client_test.go @@ -13,16 +13,19 @@ import ( "github.com/multiversx/mx-bridge-eth-go/clients" "github.com/multiversx/mx-bridge-eth-go/config" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-bridge-eth-go/core/converters" "github.com/multiversx/mx-bridge-eth-go/testsCommon" bridgeTests "github.com/multiversx/mx-bridge-eth-go/testsCommon/bridge" "github.com/multiversx/mx-bridge-eth-go/testsCommon/interactors" "github.com/multiversx/mx-bridge-eth-go/testsCommon/roleProviders" "github.com/multiversx/mx-chain-core-go/core/check" + "github.com/multiversx/mx-chain-core-go/data/transaction" "github.com/multiversx/mx-chain-core-go/data/vm" "github.com/multiversx/mx-chain-crypto-go/signing" "github.com/multiversx/mx-chain-crypto-go/signing/ed25519" logger "github.com/multiversx/mx-chain-logger-go" "github.com/multiversx/mx-sdk-go/builders" + "github.com/multiversx/mx-sdk-go/core" "github.com/multiversx/mx-sdk-go/data" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -293,7 +296,7 @@ func TestClient_GetPendingBatch(t *testing.T) { batch, err := c.GetPendingBatch(context.Background()) assert.Nil(t, batch) - assert.True(t, errors.Is(err, errNotUint64Bytes)) + assert.True(t, errors.Is(err, converters.ErrNotUint64Bytes)) assert.True(t, strings.Contains(err.Error(), "while parsing batch ID")) }) t.Run("invalid deposit nonce", func(t *testing.T) { @@ -308,7 +311,7 @@ func TestClient_GetPendingBatch(t *testing.T) { batch, err := c.GetPendingBatch(context.Background()) assert.Nil(t, batch) - assert.True(t, errors.Is(err, errNotUint64Bytes)) + assert.True(t, errors.Is(err, converters.ErrNotUint64Bytes)) assert.True(t, strings.Contains(err.Error(), "while parsing the deposit nonce, transfer index 1")) }) t.Run("tokens mapper errors", func(t *testing.T) { @@ -348,6 +351,7 @@ func TestClient_GetPendingBatch(t *testing.T) { ID: 44562, Deposits: []*bridgeCore.DepositTransfer{ { + DepositBlockNumber: 0, Nonce: 5000, ToBytes: bytes.Repeat([]byte{2}, 20), DisplayableTo: "0x0202020202020202020202020202020202020202", @@ -359,6 +363,7 @@ func TestClient_GetPendingBatch(t *testing.T) { Amount: big.NewInt(10000), }, { + DepositBlockNumber: 1, Nonce: 5001, ToBytes: bytes.Repeat([]byte{5}, 20), DisplayableTo: "0x0505050505050505050505050505050505050505", @@ -450,7 +455,7 @@ func TestClient_GetBatch(t *testing.T) { batch, err := c.GetBatch(context.Background(), 37) assert.Nil(t, batch) - assert.True(t, errors.Is(err, errNotUint64Bytes)) + assert.True(t, errors.Is(err, converters.ErrNotUint64Bytes)) assert.True(t, strings.Contains(err.Error(), "while parsing batch ID")) }) t.Run("invalid deposit nonce", func(t *testing.T) { @@ -465,7 +470,7 @@ func TestClient_GetBatch(t *testing.T) { batch, err := c.GetBatch(context.Background(), 37) assert.Nil(t, batch) - assert.True(t, errors.Is(err, errNotUint64Bytes)) + assert.True(t, errors.Is(err, converters.ErrNotUint64Bytes)) assert.True(t, strings.Contains(err.Error(), "while parsing the deposit nonce, transfer index 1")) }) t.Run("tokens mapper errors", func(t *testing.T) { @@ -505,6 +510,7 @@ func TestClient_GetBatch(t *testing.T) { ID: 44562, Deposits: []*bridgeCore.DepositTransfer{ { + DepositBlockNumber: 0, Nonce: 5000, ToBytes: bytes.Repeat([]byte{2}, 20), DisplayableTo: "0x0202020202020202020202020202020202020202", @@ -516,6 +522,7 @@ func TestClient_GetBatch(t *testing.T) { Amount: big.NewInt(10000), }, { + DepositBlockNumber: 1, Nonce: 5001, ToBytes: bytes.Repeat([]byte{5}, 20), DisplayableTo: "0x0505050505050505050505050505050505050505", @@ -1103,6 +1110,121 @@ func TestClient_CheckClientAvailability(t *testing.T) { }) } +func TestClient_GetBatchSCMetadata(t *testing.T) { + t.Parallel() + + t.Run("should error if fetching SafeContract address fails", func(t *testing.T) { + t.Parallel() + + args := createMockClientArgs() + c, _ := NewClient(args) + + expectedError := errors.New("expected error") + c.safeContractAddress = &testsCommon.AddressHandlerStub{ + AddressAsBech32StringCalled: func() (string, error) { + return "", expectedError + }, + } + + events, err := c.GetBatchSCMetadata(context.Background(), &bridgeCore.TransferBatch{}) + assert.Nil(t, events) + assert.Equal(t, expectedError, err) + }) + + t.Run("should error if fetching filter logs fails", func(t *testing.T) { + t.Parallel() + + args := createMockClientArgs() + expectedError := errors.New("expected error") + args.Proxy = &interactors.ProxyStub{ + FilterLogsCalled: func(ctx context.Context, filter *core.FilterQuery) ([]*transaction.Events, error) { + return nil, expectedError + }, + } + c, _ := NewClient(args) + + events, err := c.GetBatchSCMetadata(context.Background(), &bridgeCore.TransferBatch{}) + assert.Nil(t, events) + assert.Equal(t, expectedError, err) + }) + + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockClientArgs() + args.EventsBlockRangeFrom = -5 + args.EventsBlockRangeTo = 10 + args.Proxy = &interactors.ProxyStub{ + FilterLogsCalled: func(ctx context.Context, filter *core.FilterQuery) ([]*transaction.Events, error) { + assert.Equal(t, filter.FromBlock.HasValue, true) + assert.Equal(t, filter.FromBlock.Value, uint64(5)) + assert.Equal(t, filter.ToBlock.HasValue, true) + assert.Equal(t, filter.ToBlock.Value, uint64(30)) + return []*transaction.Events{{Identifier: "event0"}, {Identifier: "event1"}}, nil + }, + } + c, _ := NewClient(args) + + expectedBatch := &bridgeCore.TransferBatch{ + ID: 2, + Deposits: []*bridgeCore.DepositTransfer{ + { + DepositBlockNumber: 10, + Nonce: 5000, + }, + { + DepositBlockNumber: 20, + Nonce: 5001, + }, + }, + } + + events, err := c.GetBatchSCMetadata(context.Background(), expectedBatch) + assert.Nil(t, err) + assert.Equal(t, 2, len(events)) + assert.Equal(t, "event0", events[0].Identifier) + assert.Equal(t, "event1", events[1].Identifier) + }) + + t.Run("should set range boundaries to 0 in case of underflow", func(t *testing.T) { + t.Parallel() + + args := createMockClientArgs() + args.EventsBlockRangeFrom = -50 + args.EventsBlockRangeTo = 50 + args.Proxy = &interactors.ProxyStub{ + FilterLogsCalled: func(ctx context.Context, filter *core.FilterQuery) ([]*transaction.Events, error) { + assert.Equal(t, filter.FromBlock.HasValue, true) + assert.Equal(t, filter.FromBlock.Value, uint64(0)) + assert.Equal(t, filter.ToBlock.HasValue, true) + assert.Equal(t, filter.ToBlock.Value, uint64(149)) + return []*transaction.Events{{Identifier: "event0"}, {Identifier: "event1"}}, nil + }, + } + c, _ := NewClient(args) + + expectedBatch := &bridgeCore.TransferBatch{ + ID: 2, + Deposits: []*bridgeCore.DepositTransfer{ + { + DepositBlockNumber: 49, + Nonce: 5000, + }, + { + DepositBlockNumber: 99, + Nonce: 5001, + }, + }, + } + + events, err := c.GetBatchSCMetadata(context.Background(), expectedBatch) + assert.Nil(t, err) + assert.Equal(t, 2, len(events)) + assert.Equal(t, "event0", events[0].Identifier) + assert.Equal(t, "event1", events[1].Identifier) + }) +} + func resetClient(c *client) { c.mut.Lock() c.retriesAvailabilityCheck = 0 diff --git a/clients/multiversx/errors.go b/clients/multiversx/errors.go index 289ef70e..6fe5289b 100644 --- a/clients/multiversx/errors.go +++ b/clients/multiversx/errors.go @@ -8,7 +8,6 @@ var ( errNilAddressHandler = errors.New("nil address handler") errNilRequest = errors.New("nil request") errInvalidNumberOfArguments = errors.New("invalid number of arguments") - errNotUint64Bytes = errors.New("provided bytes do not represent a valid uint64 number") errInvalidGasValue = errors.New("invalid gas value") errNoStatusForBatchID = errors.New("no status for batch ID") errBatchNotFinished = errors.New("batch not finished") diff --git a/clients/multiversx/interface.go b/clients/multiversx/interface.go index 5916eb2f..76382991 100644 --- a/clients/multiversx/interface.go +++ b/clients/multiversx/interface.go @@ -22,6 +22,7 @@ type Proxy interface { GetESDTTokenData(ctx context.Context, address core.AddressHandler, tokenIdentifier string, queryOptions api.AccountQueryOptions) (*data.ESDTFungibleTokenData, error) GetTransactionInfoWithResults(ctx context.Context, hash string) (*data.TransactionInfo, error) ProcessTransactionStatus(ctx context.Context, hexTxHash string) (transaction.TxStatus, error) + FilterLogs(ctx context.Context, filter *core.FilterQuery) ([]*transaction.Events, error) IsInterfaceNil() bool } diff --git a/clients/multiversx/mxClientDataGetter.go b/clients/multiversx/mxClientDataGetter.go index aae649f0..b1613f28 100644 --- a/clients/multiversx/mxClientDataGetter.go +++ b/clients/multiversx/mxClientDataGetter.go @@ -9,6 +9,7 @@ import ( "github.com/multiversx/mx-bridge-eth-go/clients" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-bridge-eth-go/core/converters" "github.com/multiversx/mx-bridge-eth-go/errors" "github.com/multiversx/mx-chain-core-go/core/check" logger "github.com/multiversx/mx-chain-logger-go" @@ -206,7 +207,7 @@ func (dataGetter *mxClientDataGetter) ExecuteQueryReturningUint64(ctx context.Co return 0, nil } - num, err := parseUInt64FromByteSlice(response[0]) + num, err := converters.ParseUInt64FromByteSlice(response[0]) if err != nil { return 0, errors.NewQueryResponseError( internalError, @@ -238,15 +239,6 @@ func (dataGetter *mxClientDataGetter) ExecuteQueryReturningBigInt(ctx context.Co return num, nil } -func parseUInt64FromByteSlice(bytes []byte) (uint64, error) { - num := big.NewInt(0).SetBytes(bytes) - if !num.IsUint64() { - return 0, errNotUint64Bytes - } - - return num.Uint64(), nil -} - func (dataGetter *mxClientDataGetter) executeQueryFromBuilder(ctx context.Context, builder builders.VMQueryBuilder) ([][]byte, error) { vmValuesRequest, err := builder.ToVmValueRequest() if err != nil { diff --git a/clients/multiversx/mxClientDataGetter_test.go b/clients/multiversx/mxClientDataGetter_test.go index d6a9a527..3c5cb523 100644 --- a/clients/multiversx/mxClientDataGetter_test.go +++ b/clients/multiversx/mxClientDataGetter_test.go @@ -12,6 +12,7 @@ import ( "github.com/multiversx/mx-bridge-eth-go/clients" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-bridge-eth-go/core/converters" bridgeErrors "github.com/multiversx/mx-bridge-eth-go/errors" bridgeTests "github.com/multiversx/mx-bridge-eth-go/testsCommon/bridge" "github.com/multiversx/mx-bridge-eth-go/testsCommon/interactors" @@ -377,7 +378,7 @@ func TestMXClientDataGetter_ExecuteQueryReturningUint64(t *testing.T) { expectedError := bridgeErrors.NewQueryResponseError( internalError, - errNotUint64Bytes.Error(), + converters.ErrNotUint64Bytes.Error(), "", "", ) diff --git a/core/batch.go b/core/batch.go index c0907914..203e72d0 100644 --- a/core/batch.go +++ b/core/batch.go @@ -66,6 +66,7 @@ func (tb *TransferBatch) ResolveNewDeposits(newNumDeposits int) { // DepositTransfer is the deposit transfer structure agnostic of any chain implementation type DepositTransfer struct { + DepositBlockNumber uint64 `json:"depositBlockNumber"` Nonce uint64 `json:"nonce"` ToBytes []byte `json:"-"` DisplayableTo string `json:"to"` @@ -81,8 +82,8 @@ type DepositTransfer struct { // String will convert the deposit transfer to a string func (dt *DepositTransfer) String() string { - return fmt.Sprintf("to: %s, from: %s, token address: %s, amount: %v, deposit nonce: %d, data: %s", - dt.DisplayableTo, dt.DisplayableFrom, dt.DisplayableToken, dt.Amount, dt.Nonce, dt.DisplayableData) + return fmt.Sprintf("to: %s, from: %s, token address: %s, amount: %v, deposit nonce: %d, data: %s, deposit block number: %d", + dt.DisplayableTo, dt.DisplayableFrom, dt.DisplayableToken, dt.Amount, dt.Nonce, dt.DisplayableData, dt.DepositBlockNumber) } // Clone will deeply clone the current DepositTransfer instance diff --git a/core/batch_test.go b/core/batch_test.go index 48b2cef7..5129f87e 100644 --- a/core/batch_test.go +++ b/core/batch_test.go @@ -33,17 +33,18 @@ func TestDepositTransfer_String(t *testing.T) { t.Parallel() dt := &DepositTransfer{ - Nonce: 112334, - ToBytes: []byte("to"), - DisplayableTo: "to", - FromBytes: []byte("from"), - DisplayableFrom: "from", - SourceTokenBytes: []byte("source token"), - DisplayableToken: "token", - Amount: big.NewInt(7463), + DepositBlockNumber: 0, + Nonce: 112334, + ToBytes: []byte("to"), + DisplayableTo: "to", + FromBytes: []byte("from"), + DisplayableFrom: "from", + SourceTokenBytes: []byte("source token"), + DisplayableToken: "token", + Amount: big.NewInt(7463), } - expectedString := "to: to, from: from, token address: token, amount: 7463, deposit nonce: 112334, data: " + expectedString := "to: to, from: from, token address: token, amount: 7463, deposit nonce: 112334, data: , deposit block number: 0" assert.Equal(t, expectedString, dt.String()) } @@ -94,32 +95,34 @@ func TestTransferBatch_String(t *testing.T) { ID: 2243, Deposits: []*DepositTransfer{ { - Nonce: 1, - ToBytes: []byte("to1"), - DisplayableTo: "to1", - FromBytes: []byte("from1"), - DisplayableFrom: "from1", - SourceTokenBytes: []byte("source token1"), - DisplayableToken: "token1", - Amount: big.NewInt(3344), + DepositBlockNumber: 8432, + Nonce: 1, + ToBytes: []byte("to1"), + DisplayableTo: "to1", + FromBytes: []byte("from1"), + DisplayableFrom: "from1", + SourceTokenBytes: []byte("source token1"), + DisplayableToken: "token1", + Amount: big.NewInt(3344), }, { - Nonce: 2, - ToBytes: []byte("to2"), - DisplayableTo: "to2", - FromBytes: []byte("from2"), - DisplayableFrom: "from2", - SourceTokenBytes: []byte("source token2"), - DisplayableToken: "token2", - Amount: big.NewInt(5566), + DepositBlockNumber: 8433, + Nonce: 2, + ToBytes: []byte("to2"), + DisplayableTo: "to2", + FromBytes: []byte("from2"), + DisplayableFrom: "from2", + SourceTokenBytes: []byte("source token2"), + DisplayableToken: "token2", + Amount: big.NewInt(5566), }, }, Statuses: []byte{Executed, Rejected}, } expectedString := `Batch id 2243: - to: to1, from: from1, token address: token1, amount: 3344, deposit nonce: 1, data: - to: to2, from: from2, token address: token2, amount: 5566, deposit nonce: 2, data: + to: to1, from: from1, token address: token1, amount: 3344, deposit nonce: 1, data: , deposit block number: 8432 + to: to2, from: from2, token address: token2, amount: 5566, deposit nonce: 2, data: , deposit block number: 8433 Statuses: 0304` assert.Equal(t, expectedString, tb.String()) } diff --git a/core/converters/converters.go b/core/converters/converters.go index d1549019..e591a075 100644 --- a/core/converters/converters.go +++ b/core/converters/converters.go @@ -2,6 +2,7 @@ package converters import ( "encoding/hex" + "math/big" "strings" "github.com/multiversx/mx-chain-core-go/core" @@ -61,3 +62,13 @@ func TrimWhiteSpaceCharacters(input string) string { return strings.Trim(input, cutset) } + +// ParseUInt64FromByteSlice will parse the uint64 from the byte slice +func ParseUInt64FromByteSlice(bytes []byte) (uint64, error) { + num := big.NewInt(0).SetBytes(bytes) + if !num.IsUint64() { + return 0, ErrNotUint64Bytes + } + + return num.Uint64(), nil +} diff --git a/core/converters/converters_test.go b/core/converters/converters_test.go index 51033ec2..d8e0c6ae 100644 --- a/core/converters/converters_test.go +++ b/core/converters/converters_test.go @@ -83,3 +83,22 @@ func TestAddressConverter_ToHexStringWithPrefix(t *testing.T) { bytes := []byte("bytes to encode") assert.Equal(t, expected, addrConv.ToHexStringWithPrefix(bytes)) } + +func TestParseUInt64FromByteSlice(t *testing.T) { + t.Parallel() + + t.Run("large number that exceeds uint64 should return error", func(t *testing.T) { + bytes := []byte{255, 255, 255, 255, 255, 255, 255, 255, 1} // exceeds uint64 + result, err := ParseUInt64FromByteSlice(bytes) + assert.Equal(t, uint64(0), result) + assert.EqualError(t, err, ErrNotUint64Bytes.Error()) + }) + + t.Run("valid uint64 bytes should return the correct number", func(t *testing.T) { + bytes := []byte{0, 0, 0, 0, 0, 0, 1, 244} // 500 in uint64 + expected := uint64(500) + result, err := ParseUInt64FromByteSlice(bytes) + assert.Nil(t, err) + assert.Equal(t, expected, result) + }) +} diff --git a/core/converters/errors.go b/core/converters/errors.go new file mode 100644 index 00000000..d5d8e3e4 --- /dev/null +++ b/core/converters/errors.go @@ -0,0 +1,8 @@ +package converters + +import "errors" + +var ( + // ErrNotUint64Bytes is the error returned when the provided bytes do not represent a valid uint64 number + ErrNotUint64Bytes = errors.New("provided bytes do not represent a valid uint64 number") +) diff --git a/factory/ethMultiversXBridgeComponents.go b/factory/ethMultiversXBridgeComponents.go index e25e0927..23c907dc 100644 --- a/factory/ethMultiversXBridgeComponents.go +++ b/factory/ethMultiversXBridgeComponents.go @@ -306,6 +306,7 @@ func (components *ethMultiversXBridgeComponents) createMultiversXClient(args Arg } components.multiversXClient, err = multiversx.NewClient(clientArgs) + components.addClosableComponent(components.multiversXClient) return err diff --git a/go.mod b/go.mod index 34960fd0..f4824eaa 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/multiversx/mx-chain-crypto-go v1.2.11 github.com/multiversx/mx-chain-go v1.7.13-patch2 github.com/multiversx/mx-chain-logger-go v1.0.14 - github.com/multiversx/mx-sdk-go v1.4.1 + github.com/multiversx/mx-sdk-go v1.4.3 github.com/pelletier/go-toml v1.9.3 github.com/stretchr/testify v1.8.4 github.com/urfave/cli v1.22.10 diff --git a/go.sum b/go.sum index 2baca09c..7bc7d17c 100644 --- a/go.sum +++ b/go.sum @@ -562,8 +562,8 @@ github.com/multiversx/mx-chain-vm-v1_4-go v1.4.97 h1:fbYYqollxbIArcrC161Z9Qh5yJG github.com/multiversx/mx-chain-vm-v1_4-go v1.4.97/go.mod h1:56WJQio8SzOt3vWibaNkuGpqLlmTOGUSJqs3wMK69zw= github.com/multiversx/mx-components-big-int v1.0.0 h1:Wkr8lSzK2nDqixOrrBa47VNuqdhV1m/aJhaP1EMaiS8= github.com/multiversx/mx-components-big-int v1.0.0/go.mod h1:maIEMgHlNE2u78JaDD0oLzri+ShgU4okHfzP3LWGdQM= -github.com/multiversx/mx-sdk-go v1.4.1 h1:/zk7LDmnl1ovkmjkDejLRUtbjO7kmVQw8AlAyGNITBU= -github.com/multiversx/mx-sdk-go v1.4.1/go.mod h1:2kTQLFck47wtHpzdWrM3mrLlTypE5zn39JCbzN16cxs= +github.com/multiversx/mx-sdk-go v1.4.3 h1:U8ekhtQwbEN7IambCI3B65LIQTslHCAEskvcE0pzHX8= +github.com/multiversx/mx-sdk-go v1.4.3/go.mod h1:2kTQLFck47wtHpzdWrM3mrLlTypE5zn39JCbzN16cxs= github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= diff --git a/integrationTests/mock/multiversXChainMock.go b/integrationTests/mock/multiversXChainMock.go index 95a5953a..203d6f22 100644 --- a/integrationTests/mock/multiversXChainMock.go +++ b/integrationTests/mock/multiversXChainMock.go @@ -233,6 +233,11 @@ func (mock *MultiversXChainMock) GetESDTTokenData(_ context.Context, _ sdkCore.A }, nil } +// FilterLogs - +func (mock *MultiversXChainMock) FilterLogs(_ context.Context, _ *sdkCore.FilterQuery) ([]*transaction.Events, error) { + return []*transaction.Events{}, nil +} + // IsInterfaceNil - func (mock *MultiversXChainMock) IsInterfaceNil() bool { return mock == nil diff --git a/testsCommon/addressHandlerStub.go b/testsCommon/addressHandlerStub.go new file mode 100644 index 00000000..ce5f182d --- /dev/null +++ b/testsCommon/addressHandlerStub.go @@ -0,0 +1,59 @@ +package testsCommon + +type AddressHandlerStub struct { + AddressAsBech32StringCalled func() (string, error) + AddressBytesCalled func() []byte + AddressSliceCalled func() [32]byte + IsValidCalled func() bool + PrettyCalled func() string +} + +// AddressAsBech32String - +func (stub *AddressHandlerStub) AddressAsBech32String() (string, error) { + if stub.AddressAsBech32StringCalled != nil { + return stub.AddressAsBech32StringCalled() + } + + return "", nil +} + +// AddressBytes - +func (stub *AddressHandlerStub) AddressBytes() []byte { + if stub.AddressBytesCalled != nil { + return stub.AddressBytesCalled() + } + + return nil +} + +// AddressSlice - +func (stub *AddressHandlerStub) AddressSlice() [32]byte { + if stub.AddressSliceCalled != nil { + return stub.AddressSliceCalled() + } + + return [32]byte{} +} + +// IsValid - +func (stub *AddressHandlerStub) IsValid() bool { + if stub.IsValidCalled != nil { + return stub.IsValidCalled() + } + + return false +} + +// Pretty - +func (stub *AddressHandlerStub) Pretty() string { + if stub.PrettyCalled != nil { + return stub.PrettyCalled() + } + + return "" +} + +// IsInterfaceNil - +func (stub *AddressHandlerStub) IsInterfaceNil() bool { + return stub == nil +} diff --git a/testsCommon/bridge/multiversxClientStub.go b/testsCommon/bridge/multiversxClientStub.go index fd115f6a..f88e726b 100644 --- a/testsCommon/bridge/multiversxClientStub.go +++ b/testsCommon/bridge/multiversxClientStub.go @@ -6,6 +6,7 @@ import ( "math/big" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-chain-core-go/data/transaction" ) var errNotImplemented = errors.New("not implemented") @@ -38,6 +39,7 @@ type MultiversXClientStub struct { MintBalancesCalled func(ctx context.Context, token []byte) (*big.Int, error) BurnBalancesCalled func(ctx context.Context, token []byte) (*big.Int, error) CheckRequiredBalanceCalled func(ctx context.Context, token []byte, value *big.Int) error + GetBatchSCMetadataCalled func(ctx context.Context, batch *bridgeCore.TransferBatch) ([]*transaction.Events, error) CloseCalled func() error } @@ -260,6 +262,15 @@ func (stub *MultiversXClientStub) CheckRequiredBalance(ctx context.Context, toke return nil } +// GetBatchSCMetadata - +func (stub *MultiversXClientStub) GetBatchSCMetadata(ctx context.Context, batch *bridgeCore.TransferBatch) ([]*transaction.Events, error) { + if stub.GetBatchSCMetadataCalled != nil { + return stub.GetBatchSCMetadataCalled(ctx, batch) + } + + return nil, errNotImplemented +} + // Close - func (stub *MultiversXClientStub) Close() error { if stub.CloseCalled != nil { diff --git a/testsCommon/interactors/proxyStub.go b/testsCommon/interactors/proxyStub.go index 8fc8221e..57a09abb 100644 --- a/testsCommon/interactors/proxyStub.go +++ b/testsCommon/interactors/proxyStub.go @@ -22,6 +22,7 @@ type ProxyStub struct { GetESDTTokenDataCalled func(ctx context.Context, address core.AddressHandler, tokenIdentifier string, queryOptions api.AccountQueryOptions) (*data.ESDTFungibleTokenData, error) GetTransactionInfoWithResultsCalled func(_ context.Context, _ string) (*data.TransactionInfo, error) ProcessTransactionStatusCalled func(ctx context.Context, hexTxHash string) (transaction.TxStatus, error) + FilterLogsCalled func(ctx context.Context, filter *core.FilterQuery) ([]*transaction.Events, error) } // GetNetworkConfig - @@ -114,6 +115,15 @@ func (eps *ProxyStub) ProcessTransactionStatus(ctx context.Context, hexTxHash st return "", nil } +// FilterLogs - +func (eps *ProxyStub) FilterLogs(ctx context.Context, filter *core.FilterQuery) ([]*transaction.Events, error) { + if eps.FilterLogsCalled != nil { + return eps.FilterLogsCalled(ctx, filter) + } + + return nil, fmt.Errorf("not implemented") +} + // IsInterfaceNil - func (eps *ProxyStub) IsInterfaceNil() bool { return eps == nil