diff --git a/app/domain/repository/transfer.go b/app/domain/repository/transfer.go index a4f68efc1..0ff2f0c9b 100644 --- a/app/domain/repository/transfer.go +++ b/app/domain/repository/transfer.go @@ -27,6 +27,7 @@ type Transfer interface { // Returns Transfer with preloaded Fee table. Returns nil if not found GetWithFee(txId string) (*entity.Transfer, error) GetWithPreloads(txId string) (*entity.Transfer, error) + UpdateFee(txId string, fee string) error Create(ct *transfer.Transfer) (*entity.Transfer, error) UpdateStatusCompleted(txId string) error diff --git a/app/domain/service/read-only.go b/app/domain/service/read-only.go index 1ae091e66..6e2bed20d 100644 --- a/app/domain/service/read-only.go +++ b/app/domain/service/read-only.go @@ -18,8 +18,10 @@ package service import ( mirror_node "github.com/limechain/hedera-eth-bridge-validator/app/clients/hedera/mirror-node/model" + model "github.com/limechain/hedera-eth-bridge-validator/app/model/transfer" ) type ReadOnly interface { FindTransfer(transferID string, fetch func() (*mirror_node.Response, error), save func(transactionID, scheduleID, status string) error) + FindAssetTransfer(transferID string, asset string, transfers []model.Hedera, fetch func() (*mirror_node.Response, error), save func(transactionID, scheduleID, status string) error) } diff --git a/app/helper/fee/fee.go b/app/helper/fee/fee.go new file mode 100644 index 000000000..a67b0a669 --- /dev/null +++ b/app/helper/fee/fee.go @@ -0,0 +1,38 @@ +/* + * Copyright 2021 LimeChain Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fee + +import ( + "github.com/hashgraph/hedera-sdk-go/v2" + model "github.com/limechain/hedera-eth-bridge-validator/app/model/transfer" + "strconv" +) + +func GetTotalFeeFromTransfers(transfers []model.Hedera, receiver hedera.AccountID) string { + result := int64(0) + for _, transfer := range transfers { + if transfer.Amount < 0 { + continue + } + if transfer.AccountID == receiver { + continue + } + result += transfer.Amount + } + + return strconv.FormatInt(result, 10) +} diff --git a/app/persistence/entity/transfer.go b/app/persistence/entity/transfer.go index 36f156591..c70c2c640 100644 --- a/app/persistence/entity/transfer.go +++ b/app/persistence/entity/transfer.go @@ -28,9 +28,10 @@ type Transfer struct { NativeAsset string Receiver string Amount string + Fee string Status string Messages []Message `gorm:"foreignKey:TransferID"` - Fee Fee `gorm:"foreignKey:TransferID"` + Fees []Fee `gorm:"foreignKey:TransferID"` Schedules []Schedule `gorm:"foreignKey:TransferID"` } diff --git a/app/persistence/transfer/transfer.go b/app/persistence/transfer/transfer.go index 503ca4e27..c57920593 100644 --- a/app/persistence/transfer/transfer.go +++ b/app/persistence/transfer/transfer.go @@ -58,7 +58,7 @@ func (tr Repository) GetByTransactionId(txId string) (*entity.Transfer, error) { func (tr Repository) GetWithPreloads(txId string) (*entity.Transfer, error) { tx := &entity.Transfer{} result := tr.dbClient. - Preload("Fee"). + Preload("Fees"). Preload("Messages"). Model(entity.Transfer{}). Where("transaction_id = ?", txId). @@ -78,7 +78,7 @@ func (tr Repository) GetWithPreloads(txId string) (*entity.Transfer, error) { func (tr Repository) GetWithFee(txId string) (*entity.Transfer, error) { tx := &entity.Transfer{} result := tr.dbClient. - Preload("Fee"). + Preload("Fees"). Model(entity.Transfer{}). Where("transaction_id = ?", txId). First(tx) @@ -102,6 +102,18 @@ func (tr Repository) Save(tx *entity.Transfer) error { return tr.dbClient.Save(tx).Error } +func (tr Repository) UpdateFee(txId string, fee string) error { + err := tr.dbClient. + Model(entity.Transfer{}). + Where("transaction_id = ?", txId). + UpdateColumn("fee", fee). + Error + if err == nil { + tr.logger.Debugf("Updated Fee of TX [%s] to [%s]", txId, fee) + } + return err +} + func (tr Repository) UpdateStatusCompleted(txId string) error { return tr.updateStatus(txId, status.Completed) } diff --git a/app/process/handler/message-submission/handler_test.go b/app/process/handler/message-submission/handler_test.go index fc251e868..fb6aa0312 100644 --- a/app/process/handler/message-submission/handler_test.go +++ b/app/process/handler/message-submission/handler_test.go @@ -59,7 +59,7 @@ var ( Amount: tr.Amount, Status: status.Initial, Messages: nil, - Fee: entity.Fee{}, + Fees: []entity.Fee{}, Schedules: nil, } diff --git a/app/process/handler/read-only/fee-transfer/handler.go b/app/process/handler/read-only/fee-transfer/handler.go index 6fffdea7a..c34fc08bf 100644 --- a/app/process/handler/read-only/fee-transfer/handler.go +++ b/app/process/handler/read-only/fee-transfer/handler.go @@ -23,10 +23,12 @@ import ( "github.com/limechain/hedera-eth-bridge-validator/app/domain/client" "github.com/limechain/hedera-eth-bridge-validator/app/domain/repository" "github.com/limechain/hedera-eth-bridge-validator/app/domain/service" + util "github.com/limechain/hedera-eth-bridge-validator/app/helper/fee" model "github.com/limechain/hedera-eth-bridge-validator/app/model/transfer" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/schedule" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/status" + "github.com/limechain/hedera-eth-bridge-validator/app/services/fee/distributor" "github.com/limechain/hedera-eth-bridge-validator/config" log "github.com/sirupsen/logrus" "strconv" @@ -34,6 +36,7 @@ import ( // Handler is transfers event handler type Handler struct { + transferRepository repository.Transfer feeRepository repository.Fee scheduleRepository repository.Schedule mirrorNode client.MirrorNode @@ -46,6 +49,7 @@ type Handler struct { } func NewHandler( + transferRepository repository.Transfer, feeRepository repository.Fee, scheduleRepository repository.Schedule, mirrorNode client.MirrorNode, @@ -59,6 +63,7 @@ func NewHandler( log.Fatalf("Invalid account id [%s]. Error: [%s]", bridgeAccount, err) } return &Handler{ + transferRepository: transferRepository, feeRepository: feeRepository, scheduleRepository: scheduleRepository, mirrorNode: mirrorNode, @@ -78,6 +83,12 @@ func (fmh Handler) Handle(payload interface{}) { return } + receiver, err := hedera.AccountIDFromString(transferMsg.Receiver) + if err != nil { + fmh.logger.Errorf("[%s] - Failed to parse event account [%s]. Error [%s].", transferMsg.TransactionId, transferMsg.Receiver, err) + return + } + transactionRecord, err := fmh.transfersService.InitiateNewTransfer(*transferMsg) if err != nil { fmh.logger.Errorf("[%s] - Error occurred while initiating processing. Error: [%s]", transferMsg.TransactionId, err) @@ -102,36 +113,59 @@ func (fmh Handler) Handle(payload interface{}) { remainder += calculatedFee - validFee } - fmh.readOnlyService.FindTransfer(transferMsg.TransactionId, func() (*mirror_node.Response, error) { - return fmh.mirrorNode.GetAccountDebitTransactionsAfterTimestampString(fmh.bridgeAccount, transferMsg.Timestamp) - }, func(transactionID, scheduleID, status string) error { - err := fmh.scheduleRepository.Create(&entity.Schedule{ - TransactionID: transactionID, - ScheduleID: scheduleID, - Operation: schedule.TRANSFER, - Status: status, - TransferID: sql.NullString{ - String: transferMsg.TransactionId, - Valid: true, - }, + err = fmh.transferRepository.UpdateFee(transferMsg.TransactionId, strconv.FormatInt(validFee, 10)) + if err != nil { + fmh.logger.Errorf("[%s] - Failed to update fee [%d]. Error: [%s]", transferMsg.TransactionId, validFee, err) + return + } + + transfers, err := fmh.distributorService.CalculateMemberDistribution(validFee) + transfers = append(transfers, + model.Hedera{ + AccountID: receiver, + Amount: remainder, + }) + + splitTransfers := distributor.SplitAccountAmounts(transfers, + model.Hedera{ + AccountID: fmh.bridgeAccount, + Amount: -intAmount, }) - if err != nil { - fmh.logger.Errorf("[%s] - Failed to create scheduled entity [%s]. Error: [%s]", transferMsg.TransactionId, scheduleID, err) + + for _, splitTransfer := range splitTransfers { + feeAmount := util.GetTotalFeeFromTransfers(splitTransfer, receiver) + + fmh.readOnlyService.FindAssetTransfer(transferMsg.TransactionId, transferMsg.TargetAsset, splitTransfer, func() (*mirror_node.Response, error) { + return fmh.mirrorNode.GetAccountDebitTransactionsAfterTimestampString(fmh.bridgeAccount, transferMsg.Timestamp) + }, func(transactionID, scheduleID, status string) error { + err := fmh.scheduleRepository.Create(&entity.Schedule{ + TransactionID: transactionID, + ScheduleID: scheduleID, + Operation: schedule.TRANSFER, + Status: status, + TransferID: sql.NullString{ + String: transferMsg.TransactionId, + Valid: true, + }, + }) + if err != nil { + fmh.logger.Errorf("[%s] - Failed to create scheduled entity [%s]. Error: [%s]", transferMsg.TransactionId, scheduleID, err) + return err + } + err = fmh.feeRepository.Create(&entity.Fee{ + TransactionID: transactionID, + ScheduleID: scheduleID, + Amount: feeAmount, + Status: status, + TransferID: sql.NullString{ + String: transferMsg.TransactionId, + Valid: true, + }, + }) + if err != nil { + fmh.logger.Errorf("[%s] - Failed to create fee entity [%s]. Error: [%s]", transferMsg.TransactionId, scheduleID, err) + } return err - } - err = fmh.feeRepository.Create(&entity.Fee{ - TransactionID: transactionID, - ScheduleID: scheduleID, - Amount: strconv.FormatInt(validFee, 10), - Status: status, - TransferID: sql.NullString{ - String: transferMsg.TransactionId, - Valid: true, - }, }) - if err != nil { - fmh.logger.Errorf("[%s] - Failed to create fee entity [%s]. Error: [%s]", transferMsg.TransactionId, scheduleID, err) - } - return err - }) + } } diff --git a/app/process/handler/read-only/fee-transfer/handler_test.go b/app/process/handler/read-only/fee-transfer/handler_test.go index 9cd5381ee..b4de630ff 100644 --- a/app/process/handler/read-only/fee-transfer/handler_test.go +++ b/app/process/handler/read-only/fee-transfer/handler_test.go @@ -53,7 +53,16 @@ var ( func Test_NewHandler(t *testing.T) { setup() - assert.Equal(t, h, NewHandler(mocks.MFeeRepository, mocks.MScheduleRepository, mocks.MHederaMirrorClient, accountId.String(), mocks.MDistributorService, mocks.MFeeService, mocks.MTransferService, mocks.MReadOnlyService)) + assert.Equal(t, h, NewHandler( + mocks.MTransferRepository, + mocks.MFeeRepository, + mocks.MScheduleRepository, + mocks.MHederaMirrorClient, + accountId.String(), + mocks.MDistributorService, + mocks.MFeeService, + mocks.MTransferService, + mocks.MReadOnlyService)) } func Test_Handle(t *testing.T) { @@ -70,13 +79,13 @@ func Test_Handle(t *testing.T) { Amount: "100", Status: status.Initial, Messages: nil, - Fee: entity.Fee{}, + Fees: []entity.Fee{}, Schedules: nil, } mocks.MTransferService.On("InitiateNewTransfer", *tr).Return(tr, nil) mocks.MFeeService.On("CalculateFee", tr.TargetAsset, int64(100)).Return(int64(10), int64(0)) mocks.MDistributorService.On("ValidAmount", 10).Return(int64(3)) - mocks.MReadOnlyService.On("FindTransfer", mock.Anything, mock.Anything, mock.Anything) + mocks.MReadOnlyService.On("FindAssetTransfer", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) h.Handle(tr) } @@ -85,7 +94,9 @@ func Test_Handle_FindTransfer(t *testing.T) { mocks.MTransferService.On("InitiateNewTransfer", *tr).Return(&entity.Transfer{Status: status.Initial}, nil) mocks.MFeeService.On("CalculateFee", tr.TargetAsset, int64(100)).Return(int64(10), int64(0)) mocks.MDistributorService.On("ValidAmount", int64(10)).Return(int64(3)) - mocks.MReadOnlyService.On("FindTransfer", mock.Anything, mock.Anything, mock.Anything) + mocks.MTransferRepository.On("UpdateFee", tr.TransactionId, "3").Return(nil) + mocks.MDistributorService.On("CalculateMemberDistribution", int64(3)).Return([]model.Hedera{}) + mocks.MReadOnlyService.On("FindAssetTransfer", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) h.Handle(tr) } @@ -119,6 +130,7 @@ func Test_Handle_InitiateNewTransferFails(t *testing.T) { func setup() { mocks.Setup() h = &Handler{ + transferRepository: mocks.MTransferRepository, feeRepository: mocks.MFeeRepository, scheduleRepository: mocks.MScheduleRepository, mirrorNode: mocks.MHederaMirrorClient, diff --git a/app/process/handler/read-only/fee/handler.go b/app/process/handler/read-only/fee/handler.go index 83d0d814d..e5dc51842 100644 --- a/app/process/handler/read-only/fee/handler.go +++ b/app/process/handler/read-only/fee/handler.go @@ -27,6 +27,7 @@ import ( "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/schedule" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/status" + "github.com/limechain/hedera-eth-bridge-validator/app/services/fee/distributor" "github.com/limechain/hedera-eth-bridge-validator/config" log "github.com/sirupsen/logrus" "strconv" @@ -34,6 +35,7 @@ import ( // Handler is transfers event handler type Handler struct { + transferRepository repository.Transfer feeRepository repository.Fee scheduleRepository repository.Schedule mirrorNode client.MirrorNode @@ -46,6 +48,7 @@ type Handler struct { } func NewHandler( + transferRepository repository.Transfer, feeRepository repository.Fee, scheduleRepository repository.Schedule, mirrorNode client.MirrorNode, @@ -59,6 +62,7 @@ func NewHandler( log.Fatalf("Invalid account id [%s]. Error: [%s]", bridgeAccount, err) } return &Handler{ + transferRepository: transferRepository, feeRepository: feeRepository, scheduleRepository: scheduleRepository, mirrorNode: mirrorNode, @@ -98,38 +102,56 @@ func (fmh Handler) Handle(payload interface{}) { calculatedFee, _ := fmh.feeService.CalculateFee(transferMsg.SourceAsset, intAmount) validFee := fmh.distributor.ValidAmount(calculatedFee) - fmh.readOnlyService.FindTransfer(transferMsg.TransactionId, - func() (*mirror_node.Response, error) { - return fmh.mirrorNode.GetAccountDebitTransactionsAfterTimestampString(fmh.bridgeAccount, transferMsg.Timestamp) - }, - func(transactionID, scheduleID, status string) error { - err := fmh.scheduleRepository.Create(&entity.Schedule{ - TransactionID: transactionID, - ScheduleID: scheduleID, - Operation: schedule.TRANSFER, - Status: status, - TransferID: sql.NullString{ - String: transferMsg.TransactionId, - Valid: true, - }, - }) - if err != nil { - fmh.logger.Errorf("[%s] - Failed to create scheduled entity [%s]. Error: [%s]", transferMsg.TransactionId, scheduleID, err) + err = fmh.transferRepository.UpdateFee(transferMsg.TransactionId, strconv.FormatInt(validFee, 10)) + if err != nil { + fmh.logger.Errorf("[%s] - Failed to update fee [%d]. Error: [%s]", transferMsg.TransactionId, validFee, err) + return + } + + transfers, err := fmh.distributor.CalculateMemberDistribution(validFee) + + splitTransfers := distributor.SplitAccountAmounts(transfers, + model.Hedera{ + AccountID: fmh.bridgeAccount, + Amount: -validFee, + }) + + for _, splitTransfer := range splitTransfers { + feeAmount := -splitTransfer[len(splitTransfer)-1].Amount + + fmh.readOnlyService.FindAssetTransfer(transferMsg.TransactionId, transferMsg.NativeAsset, splitTransfer, + func() (*mirror_node.Response, error) { + return fmh.mirrorNode.GetAccountDebitTransactionsAfterTimestampString(fmh.bridgeAccount, transferMsg.Timestamp) + }, + func(transactionID, scheduleID, status string) error { + err := fmh.scheduleRepository.Create(&entity.Schedule{ + TransactionID: transactionID, + ScheduleID: scheduleID, + Operation: schedule.TRANSFER, + Status: status, + TransferID: sql.NullString{ + String: transferMsg.TransactionId, + Valid: true, + }, + }) + if err != nil { + fmh.logger.Errorf("[%s] - Failed to create scheduled entity [%s]. Error: [%s]", transferMsg.TransactionId, scheduleID, err) + return err + } + err = fmh.feeRepository.Create(&entity.Fee{ + TransactionID: transactionID, + ScheduleID: scheduleID, + Amount: strconv.FormatInt(feeAmount, 10), + Status: status, + TransferID: sql.NullString{ + String: transferMsg.TransactionId, + Valid: true, + }, + }) + if err != nil { + fmh.logger.Errorf("[%s] - Failed to create fee entity [%s]. Error: [%s]", transferMsg.TransactionId, scheduleID, err) + } return err - } - err = fmh.feeRepository.Create(&entity.Fee{ - TransactionID: transactionID, - ScheduleID: scheduleID, - Amount: strconv.FormatInt(validFee, 10), - Status: status, - TransferID: sql.NullString{ - String: transferMsg.TransactionId, - Valid: true, - }, }) - if err != nil { - fmh.logger.Errorf("[%s] - Failed to create fee entity [%s]. Error: [%s]", transferMsg.TransactionId, scheduleID, err) - } - return err - }) + } } diff --git a/app/process/handler/read-only/fee/handler_test.go b/app/process/handler/read-only/fee/handler_test.go index fe8e9e496..271aab084 100644 --- a/app/process/handler/read-only/fee/handler_test.go +++ b/app/process/handler/read-only/fee/handler_test.go @@ -53,7 +53,16 @@ var ( func Test_NewHandler(t *testing.T) { setup() - assert.Equal(t, h, NewHandler(mocks.MFeeRepository, mocks.MScheduleRepository, mocks.MHederaMirrorClient, accountId.String(), mocks.MDistributorService, mocks.MFeeService, mocks.MTransferService, mocks.MReadOnlyService)) + assert.Equal(t, h, NewHandler( + mocks.MTransferRepository, + mocks.MFeeRepository, + mocks.MScheduleRepository, + mocks.MHederaMirrorClient, + accountId.String(), + mocks.MDistributorService, + mocks.MFeeService, + mocks.MTransferService, + mocks.MReadOnlyService)) } func Test_Handle(t *testing.T) { @@ -70,13 +79,13 @@ func Test_Handle(t *testing.T) { Amount: "100", Status: status.Initial, Messages: nil, - Fee: entity.Fee{}, + Fees: []entity.Fee{}, Schedules: nil, } mocks.MTransferService.On("InitiateNewTransfer", *tr).Return(tr, nil) mocks.MFeeService.On("CalculateFee", tr.SourceAsset, int64(100)).Return(int64(10), int64(0)) mocks.MDistributorService.On("ValidAmount", 10).Return(int64(3)) - mocks.MReadOnlyService.On("FindTransfer", mock.Anything, mock.Anything, mock.Anything) + mocks.MReadOnlyService.On("FindAssetTransfer", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) h.Handle(tr) } @@ -85,7 +94,9 @@ func Test_Handle_FindTransfer(t *testing.T) { mocks.MTransferService.On("InitiateNewTransfer", *tr).Return(&entity.Transfer{Status: status.Initial}, nil) mocks.MFeeService.On("CalculateFee", tr.SourceAsset, int64(100)).Return(int64(10), int64(0)) mocks.MDistributorService.On("ValidAmount", int64(10)).Return(int64(3)) - mocks.MReadOnlyService.On("FindTransfer", mock.Anything, mock.Anything, mock.Anything) + mocks.MTransferRepository.On("UpdateFee", tr.TransactionId, "3").Return(nil) + mocks.MDistributorService.On("CalculateMemberDistribution", int64(3)).Return([]model.Hedera{}, nil) + mocks.MReadOnlyService.On("FindAssetTransfer", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) h.Handle(tr) } @@ -119,6 +130,7 @@ func Test_Handle_InitiateNewTransferFails(t *testing.T) { func setup() { mocks.Setup() h = &Handler{ + transferRepository: mocks.MTransferRepository, feeRepository: mocks.MFeeRepository, scheduleRepository: mocks.MScheduleRepository, mirrorNode: mocks.MHederaMirrorClient, diff --git a/app/services/burn-event/service.go b/app/services/burn-event/service.go index d5850c91e..11f9f517f 100644 --- a/app/services/burn-event/service.go +++ b/app/services/burn-event/service.go @@ -21,10 +21,12 @@ import ( "github.com/hashgraph/hedera-sdk-go/v2" "github.com/limechain/hedera-eth-bridge-validator/app/domain/repository" "github.com/limechain/hedera-eth-bridge-validator/app/domain/service" + util "github.com/limechain/hedera-eth-bridge-validator/app/helper/fee" "github.com/limechain/hedera-eth-bridge-validator/app/model/transfer" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/schedule" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/status" + "github.com/limechain/hedera-eth-bridge-validator/app/services/fee/distributor" "github.com/limechain/hedera-eth-bridge-validator/config" log "github.com/sirupsen/logrus" "strconv" @@ -86,19 +88,28 @@ func (s Service) ProcessEvent(event transfer.Transfer) { return } - _, feeAmount, transfers, err := s.prepareTransfers(event.NativeAsset, amount, receiver) + fee, splitTransfers, err := s.prepareTransfers(event.NativeAsset, amount, receiver) if err != nil { s.logger.Errorf("[%s] - Failed to prepare transfers. Error [%s].", event.TransactionId, err) return } - onExecutionSuccess, onExecutionFail := s.scheduledTxExecutionCallbacks(event.TransactionId, strconv.FormatInt(feeAmount, 10)) - onSuccess, onFail := s.scheduledTxMinedCallbacks(event.TransactionId) + err = s.repository.UpdateFee(event.TransactionId, strconv.FormatInt(fee, 10)) + if err != nil { + s.logger.Errorf("[%s] - Failed to update fee [%d]. Error [%s].", event.TransactionId, fee, err) + return + } + + for _, splitTransfer := range splitTransfers { + feeAmount := util.GetTotalFeeFromTransfers(splitTransfer, receiver) + onExecutionSuccess, onExecutionFail := s.scheduledTxExecutionCallbacks(event.TransactionId, feeAmount) + onSuccess, onFail := s.scheduledTxMinedCallbacks(event.TransactionId) - s.scheduledService.ExecuteScheduledTransferTransaction(event.TransactionId, event.NativeAsset, transfers, onExecutionSuccess, onExecutionFail, onSuccess, onFail) + s.scheduledService.ExecuteScheduledTransferTransaction(event.TransactionId, event.NativeAsset, splitTransfer, onExecutionSuccess, onExecutionFail, onSuccess, onFail) + } } -func (s *Service) prepareTransfers(token string, amount int64, receiver hedera.AccountID) (recipientAmount int64, feeAmount int64, transfers []transfer.Hedera, err error) { +func (s *Service) prepareTransfers(token string, amount int64, receiver hedera.AccountID) (fee int64, splitTransfers [][]transfer.Hedera, err error) { fee, remainder := s.feeService.CalculateFee(token, amount) validFee := s.distributorService.ValidAmount(fee) @@ -106,22 +117,24 @@ func (s *Service) prepareTransfers(token string, amount int64, receiver hedera.A remainder += fee - validFee } - transfers, err = s.distributorService.CalculateMemberDistribution(validFee) + transfers, err := s.distributorService.CalculateMemberDistribution(validFee) if err != nil { - return 0, 0, nil, err + return 0, nil, err } transfers = append(transfers, transfer.Hedera{ AccountID: receiver, Amount: remainder, - }, + }) + + splitTransfers = distributor.SplitAccountAmounts(transfers, transfer.Hedera{ AccountID: s.bridgeAccount, Amount: -amount, }) - return remainder, validFee, transfers, nil + return validFee, splitTransfers, nil } // TransactionID returns the corresponding Scheduled Transaction paying out the diff --git a/app/services/burn-event/service_test.go b/app/services/burn-event/service_test.go index c18a16f9c..53edd9b57 100644 --- a/app/services/burn-event/service_test.go +++ b/app/services/burn-event/service_test.go @@ -68,7 +68,7 @@ var ( Amount: tr.Amount, Status: status.Initial, Messages: nil, - Fee: entity.Fee{}, + Fees: []entity.Fee{}, Schedules: nil, } ) @@ -94,6 +94,7 @@ func Test_ProcessEvent(t *testing.T) { mocks.MFeeService.On("CalculateFee", tr.NativeAsset, burnEventAmount).Return(mockFee, mockRemainder) mocks.MDistributorService.On("ValidAmount", mockFee).Return(mockValidFee) mocks.MDistributorService.On("CalculateMemberDistribution", mockValidFee).Return([]transfer.Hedera{}, nil) + mocks.MTransferRepository.On("UpdateFee", tr.TransactionId, strconv.FormatInt(mockValidFee, 10)).Return(nil) mocks.MScheduledService.On("ExecuteScheduledTransferTransaction", tr.TransactionId, tr.NativeAsset, mockTransfersAfterPreparation).Return() s.ProcessEvent(tr) diff --git a/app/services/fee/distributor/distributor.go b/app/services/fee/distributor/distributor.go index 59c85ff8e..60e705bc0 100644 --- a/app/services/fee/distributor/distributor.go +++ b/app/services/fee/distributor/distributor.go @@ -31,6 +31,8 @@ type Service struct { logger *log.Entry } +const TotalPositiveTransfersPerTransaction = 9 + func New(members []string) *Service { if len(members) == 0 { log.Fatal("No members accounts provided") @@ -71,6 +73,38 @@ func (s Service) CalculateMemberDistribution(amount int64) ([]transfer.Hedera, e return transfers, nil } +// SplitAccountAmounts splits account amounts to a chunks of TotalPositiveTransfersPerTransaction + 1 +// (1 comes from the negative account amount, opposite to the sum of the positive account amounts) +// It is necessary, because at this given moment, Hedera does not support a transfer transaction with +// a transfer list exceeding (TotalPositiveTransfersPerTransaction + 1) +func SplitAccountAmounts(positiveAccountAmounts []transfer.Hedera, negativeAccountAmount transfer.Hedera) [][]transfer.Hedera { + totalLength := len(positiveAccountAmounts) + + if totalLength <= TotalPositiveTransfersPerTransaction { + transfers := append(positiveAccountAmounts, negativeAccountAmount) + + return [][]transfer.Hedera{transfers} + } else { + splits := (totalLength + TotalPositiveTransfersPerTransaction - 1) / TotalPositiveTransfersPerTransaction + result := make([][]transfer.Hedera, splits) + + previous := 0 + for i := 0; previous < totalLength; i++ { + next := previous + TotalPositiveTransfersPerTransaction + if next > totalLength { + next = totalLength + } + transfers := make([]transfer.Hedera, next-previous) + copy(transfers, positiveAccountAmounts[previous:next]) + transfers = append(transfers, transfer.Hedera{AccountID: negativeAccountAmount.AccountID, Amount: calculateOppositeNegative(transfers)}) + result[i] = transfers + previous = next + } + + return result + } +} + func (s Service) PrepareTransfers(amount int64, token string) ([]model.Transfer, error) { feePerAccount := amount / int64(len(s.accountIDs)) @@ -110,3 +144,13 @@ func (s Service) ValidAmount(amount int64) int64 { return amount } + +// Sums the amounts and returns the opposite +func calculateOppositeNegative(transfers []transfer.Hedera) int64 { + negatedValue := int64(0) + for _, transfer := range transfers { + negatedValue += transfer.Amount + } + + return -negatedValue +} diff --git a/app/services/fee/distributor/distributor_test.go b/app/services/fee/distributor/distributor_test.go new file mode 100644 index 000000000..16be23708 --- /dev/null +++ b/app/services/fee/distributor/distributor_test.go @@ -0,0 +1,164 @@ +/* + * Copyright 2021 LimeChain Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package distributor + +import ( + "github.com/hashgraph/hedera-sdk-go/v2" + "github.com/limechain/hedera-eth-bridge-validator/app/model/transfer" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_SplitTransfersBelowTotal(t *testing.T) { + length := 6 + positiveAccountAmounts := make([]transfer.Hedera, length) + for i := 0; i < length; i++ { + positiveAccountAmounts[i] = transfer.Hedera{ + AccountID: hedera.AccountID{0, 0, uint64(i)}, + Amount: 1, + } + } + negativeAccountAmount := transfer.Hedera{ + AccountID: hedera.AccountID{0, 0, 7}, + Amount: int64(-length), + } + expected := [][]transfer.Hedera{ + append(positiveAccountAmounts, negativeAccountAmount), + } + + // when: + result := SplitAccountAmounts(positiveAccountAmounts, negativeAccountAmount) + + // then: + assert.Equal(t, 1, len(result)) + assert.Equal(t, expected, result) + assert.Equal(t, 7, len(result[0])) +} + +func Test_SplitTransfersExactLength(t *testing.T) { + length := 9 + positiveAccountAmounts := make([]transfer.Hedera, length) + for i := 0; i < length; i++ { + positiveAccountAmounts[i] = transfer.Hedera{ + AccountID: hedera.AccountID{0, 0, uint64(i)}, + Amount: 1, + } + } + negativeAccountAmount := transfer.Hedera{ + AccountID: hedera.AccountID{0, 0, 7}, + Amount: int64(-length), + } + expected := [][]transfer.Hedera{ + append(positiveAccountAmounts, negativeAccountAmount), + } + + // when: + result := SplitAccountAmounts(positiveAccountAmounts, negativeAccountAmount) + + // then: + assert.Equal(t, 1, len(result)) + assert.Equal(t, expected, result) + assert.Equal(t, 10, len(result[0])) +} + +func Test_SplitTransfersAboveTotalTransfersPerTransaction(t *testing.T) { + length := 11 + positiveAccountAmounts := make([]transfer.Hedera, length) + for i := 0; i < length; i++ { + positiveAccountAmounts[i] = transfer.Hedera{ + AccountID: hedera.AccountID{0, 0, uint64(i)}, + Amount: 1, + } + } + negativeAccountAmount := transfer.Hedera{ + AccountID: hedera.AccountID{0, 0, 7}, + Amount: int64(-length), + } + expectedSplit := (length + TotalPositiveTransfersPerTransaction - 1) / TotalPositiveTransfersPerTransaction + expectedChunkOneLength := 10 + expectedChunkTwoLength := 3 + + // when: + result := SplitAccountAmounts(positiveAccountAmounts, negativeAccountAmount) + + // then: + assert.Equal(t, expectedSplit, len(result)) + assert.Equal(t, expectedChunkOneLength, len(result[0])) + // and: + for i := 0; i < expectedChunkOneLength-1; i++ { + assert.Equal(t, positiveAccountAmounts[i], result[0][i]) + } + assert.Equal(t, transfer.Hedera{ + AccountID: negativeAccountAmount.AccountID, + Amount: int64(-9), + }, result[0][expectedChunkOneLength-1]) + + // and: + assert.Equal(t, expectedChunkTwoLength, len(result[1])) + for i := 0; i < expectedChunkTwoLength-1; i++ { + assert.Equal(t, positiveAccountAmounts[TotalPositiveTransfersPerTransaction+i], result[1][i]) + } + + assert.Equal(t, transfer.Hedera{ + AccountID: negativeAccountAmount.AccountID, + Amount: int64(-2), + }, result[1][expectedChunkTwoLength-1]) +} + +func Test_SplitTransfersAboveTotalTransfersEquallyDivided(t *testing.T) { + length := 18 + positiveAccountAmounts := make([]transfer.Hedera, length) + for i := 0; i < length; i++ { + positiveAccountAmounts[i] = transfer.Hedera{ + AccountID: hedera.AccountID{0, 0, uint64(i)}, + Amount: 1, + } + } + negativeAccountAmount := transfer.Hedera{ + AccountID: hedera.AccountID{0, 0, 7}, + Amount: int64(-length), + } + expectedSplit := (length + TotalPositiveTransfersPerTransaction - 1) / TotalPositiveTransfersPerTransaction + expectedChunkOneLength := 10 + expectedChunkTwoLength := 10 + + // when: + result := SplitAccountAmounts(positiveAccountAmounts, negativeAccountAmount) + + // then: + assert.Equal(t, expectedSplit, len(result)) + assert.Equal(t, expectedChunkOneLength, len(result[0])) + // and: + for i := 0; i < expectedChunkOneLength-1; i++ { + assert.Equal(t, positiveAccountAmounts[i], result[0][i]) + } + assert.Equal(t, transfer.Hedera{ + AccountID: negativeAccountAmount.AccountID, + Amount: int64(-9), + }, result[0][expectedChunkOneLength-1]) + + // and: + assert.Equal(t, expectedChunkTwoLength, len(result[1])) + for i := 0; i < expectedChunkTwoLength-1; i++ { + assert.Equal(t, positiveAccountAmounts[TotalPositiveTransfersPerTransaction+i], result[1][i]) + } + + assert.Equal(t, transfer.Hedera{ + AccountID: negativeAccountAmount.AccountID, + Amount: int64(-9), + }, result[1][expectedChunkTwoLength-1]) +} diff --git a/app/services/messages/service.go b/app/services/messages/service.go index b8cec8dce..723115f97 100644 --- a/app/services/messages/service.go +++ b/app/services/messages/service.go @@ -96,7 +96,7 @@ func (ss *Service) SanityCheckSignature(topicMessage message.Message) (bool, err return false, err } - feeAmount, err := strconv.ParseInt(t.Fee.Amount, 10, 64) + feeAmount, err := strconv.ParseInt(t.Fee, 10, 64) if err != nil { ss.logger.Errorf("[%s] - Failed to parse fee amount. Error [%s]", topicMessage.TransferID, err) return false, err @@ -214,7 +214,7 @@ func (ss *Service) verifySignature(err error, authMsgBytes []byte, signatureByte // awaitTransfer checks until given transfer is found func (ss *Service) awaitTransfer(transferID string) (*entity.Transfer, error) { for { - t, err := ss.transferRepository.GetWithFee(transferID) + t, err := ss.transferRepository.GetByTransactionId(transferID) if err != nil { ss.logger.Errorf("[%s] - Failed to retrieve Transaction Record. Error: [%s]", transferID, err) return nil, err @@ -224,7 +224,7 @@ func (ss *Service) awaitTransfer(transferID string) (*entity.Transfer, error) { if t.NativeChainID != 0 { return t, nil } - if t.NativeChainID == 0 && t.Fee.TransactionID != "" { + if t.NativeChainID == 0 && t.Fee != "" { return t, nil } } diff --git a/app/services/read-only/service.go b/app/services/read-only/service.go index 0bb6d4d50..3d41209cf 100644 --- a/app/services/read-only/service.go +++ b/app/services/read-only/service.go @@ -21,8 +21,10 @@ import ( mirror_node "github.com/limechain/hedera-eth-bridge-validator/app/clients/hedera/mirror-node/model" "github.com/limechain/hedera-eth-bridge-validator/app/domain/client" "github.com/limechain/hedera-eth-bridge-validator/app/domain/repository" + model "github.com/limechain/hedera-eth-bridge-validator/app/model/transfer" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/status" "github.com/limechain/hedera-eth-bridge-validator/config" + "github.com/limechain/hedera-eth-bridge-validator/constants" log "github.com/sirupsen/logrus" ) @@ -42,6 +44,72 @@ func New( } } +func (s Service) FindAssetTransfer( + transferID string, + asset string, + expectedTransfers []model.Hedera, + fetch func() (*mirror_node.Response, error), + save func(transactionID, scheduleID, status string) error) { + for { + response, err := fetch() + if err != nil { + s.logger.Errorf("[%s] - Failed to get token burn transactions after timestamp. Error: [%s]", transferID, err) + continue + } + + finished := false + for _, transaction := range response.Transactions { + isFound := false + scheduledTx, err := s.mirrorNode.GetScheduledTransaction(transaction.TransactionID) + if err != nil { + s.logger.Errorf("[%s] - Failed to retrieve scheduled transaction [%s]. Error: [%s]", transferID, transaction.TransactionID, err) + continue + } + for _, tx := range scheduledTx.Transactions { + if tx.Result == hedera.StatusSuccess.String() { + scheduleID, err := s.mirrorNode.GetSchedule(tx.EntityId) + if err != nil { + s.logger.Errorf("[%s] - Failed to get scheduled entity [%s]. Error: [%s]", transferID, scheduleID, err) + break + } + if scheduleID.Memo == transferID { + isFound = true + } + } + if isFound && transfersAreFound(expectedTransfers, asset, transaction) { + s.logger.Infof("[%s] - Found a corresponding transaction [%s], ScheduleID [%s].", transferID, transaction.TransactionID, tx.EntityId) + finished = true + isSuccessful := transaction.Result == hedera.StatusSuccess.String() + txStatus := status.Completed + if !isSuccessful { + txStatus = status.Failed + } + + err := save(transaction.TransactionID, tx.EntityId, txStatus) + if err != nil { + s.logger.Errorf("[%s] - Failed to save entity [%s]. Error: [%s]", transferID, tx.EntityId, err) + break + } + + if isSuccessful { + err = s.transferRepository.UpdateStatusCompleted(transferID) + } else { + err = s.transferRepository.UpdateStatusFailed(transferID) + } + if err != nil { + s.logger.Errorf("[%s] - Failed to update status. Error: [%s]", transferID, err) + break + } + break + } + } + } + if finished { + break + } + } +} + func (s Service) FindTransfer( transferID string, fetch func() (*mirror_node.Response, error), @@ -105,3 +173,33 @@ func (s Service) FindTransfer( } } } + +func transfersAreFound(expectedTransfers []model.Hedera, asset string, transaction mirror_node.Transaction) bool { + for _, expectedTransfer := range expectedTransfers { + found := false + if asset == constants.Hbar { + for _, transfer := range transaction.Transfers { + if expectedTransfer.AccountID.String() == transfer.Account && + expectedTransfer.Amount == transfer.Amount { + found = true + break + } + } + } else { + for _, transfer := range transaction.TokenTransfers { + if expectedTransfer.AccountID.String() == transfer.Account && + expectedTransfer.Amount == transfer.Amount && + asset == transfer.Token { + found = true + break + } + } + } + + if !found { + return false + } + } + + return true +} diff --git a/app/services/transfers/service.go b/app/services/transfers/service.go index 65ca125d2..83624fa28 100644 --- a/app/services/transfers/service.go +++ b/app/services/transfers/service.go @@ -36,6 +36,7 @@ import ( "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/schedule" "github.com/limechain/hedera-eth-bridge-validator/app/persistence/entity/status" + "github.com/limechain/hedera-eth-bridge-validator/app/services/fee/distributor" "github.com/limechain/hedera-eth-bridge-validator/config" log "github.com/sirupsen/logrus" "math/big" @@ -243,23 +244,31 @@ func (ts *Service) submitTopicMessageAndWaitForTransaction(signatureMessage *mes return nil } -func (ts *Service) processFeeTransfer(transferID string, feeAmount int64, nativeAsset string) { - transfers, err := ts.distributor.CalculateMemberDistribution(feeAmount) +func (ts *Service) processFeeTransfer(transferID string, totalFee int64, nativeAsset string) { + transfers, err := ts.distributor.CalculateMemberDistribution(totalFee) if err != nil { ts.logger.Errorf("[%s] Fee - Failed to Distribute to Members. Error: [%s].", transferID, err) return } - transfers = append(transfers, - model.Hedera{ - AccountID: ts.bridgeAccountID, - Amount: -feeAmount, - }) + splitTransfers := distributor.SplitAccountAmounts(transfers, model.Hedera{ + AccountID: ts.bridgeAccountID, + Amount: -totalFee, + }) + + err = ts.transferRepository.UpdateFee(transferID, strconv.FormatInt(totalFee, 10)) + if err != nil { + ts.logger.Errorf("[%s] - Failed to update fee [%d]. Error [%s].", transferID, totalFee, err) + return + } - onExecutionSuccess, onExecutionFail := ts.scheduledTxExecutionCallbacks(transferID, strconv.FormatInt(feeAmount, 10)) - onSuccess, onFail := ts.scheduledTxMinedCallbacks() + for _, splitTransfer := range splitTransfers { + fee := -splitTransfer[len(splitTransfer)-1].Amount + onExecutionSuccess, onExecutionFail := ts.scheduledTxExecutionCallbacks(transferID, strconv.FormatInt(fee, 10)) + onSuccess, onFail := ts.scheduledTxMinedCallbacks() - ts.scheduledService.ExecuteScheduledTransferTransaction(transferID, nativeAsset, transfers, onExecutionSuccess, onExecutionFail, onSuccess, onFail) + ts.scheduledService.ExecuteScheduledTransferTransaction(transferID, nativeAsset, splitTransfer, onExecutionSuccess, onExecutionFail, onSuccess, onFail) + } } func (ts *Service) scheduledBurnTxExecutionCallbacks(transferID string, blocker *chan string) (onExecutionSuccess func(transactionID string, scheduleID string), onExecutionFail func(transactionID string)) { @@ -458,7 +467,7 @@ func (ts *Service) TransferData(txId string) (service.TransferData, error) { return service.TransferData{}, service.ErrNotFound } - if t != nil && t.NativeChainID == 0 && t.Fee.Amount == "" { + if t != nil && t.NativeChainID == 0 && t.Fee == "" { return service.TransferData{}, service.ErrNotFound } @@ -470,7 +479,7 @@ func (ts *Service) TransferData(txId string) (service.TransferData, error) { return service.TransferData{}, err } - feeAmount, err := strconv.ParseInt(t.Fee.Amount, 10, 64) + feeAmount, err := strconv.ParseInt(t.Fee, 10, 64) if err != nil { ts.logger.Errorf("[%s] - Failed to parse fee amount. Error [%s]", t.TransactionID, err) return service.TransferData{}, err diff --git a/cmd/main.go b/cmd/main.go index 738612a11..5d00c94d4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -141,6 +141,7 @@ func initializeServerPairs(server *server.Server, services *Services, repositori // Register read-only handlers server.AddHandler(constants.ReadOnlyHederaTransfer, rfth.NewHandler( + repositories.transfer, repositories.fee, repositories.schedule, clients.MirrorNode, @@ -150,6 +151,7 @@ func initializeServerPairs(server *server.Server, services *Services, repositori services.transfers, services.readOnly)) server.AddHandler(constants.ReadOnlyHederaFeeTransfer, rfh.NewHandler( + repositories.transfer, repositories.fee, repositories.schedule, clients.MirrorNode, diff --git a/test/mocks/repository/transfer_repository_mock.go b/test/mocks/repository/transfer_repository_mock.go index 3683a9ea1..59cedd545 100644 --- a/test/mocks/repository/transfer_repository_mock.go +++ b/test/mocks/repository/transfer_repository_mock.go @@ -54,6 +54,15 @@ func (m *MockTransferRepository) Create(ct *transfer.Transfer) (*entity.Transfer return nil, args.Get(1).(error) } +func (m *MockTransferRepository) UpdateFee(txId, fee string) error { + args := m.Called(txId, fee) + if args.Get(0) == nil { + return nil + } + + return args.Get(0).(error) +} + func (m *MockTransferRepository) UpdateStatusCompleted(txId string) error { args := m.Called(txId) if args.Get(0) == nil { diff --git a/test/mocks/service/readonly_service_mock.go b/test/mocks/service/readonly_service_mock.go index aac0f6ad6..3738ece72 100644 --- a/test/mocks/service/readonly_service_mock.go +++ b/test/mocks/service/readonly_service_mock.go @@ -18,6 +18,7 @@ package service import ( mirror_node "github.com/limechain/hedera-eth-bridge-validator/app/clients/hedera/mirror-node/model" + "github.com/limechain/hedera-eth-bridge-validator/app/model/transfer" "github.com/stretchr/testify/mock" ) @@ -28,3 +29,7 @@ type MockReadOnlyService struct { func (m *MockReadOnlyService) FindTransfer(transferID string, fetch func() (*mirror_node.Response, error), save func(transactionID, scheduleID, status string) error) { m.Called(transferID, fetch, save) } + +func (m *MockReadOnlyService) FindAssetTransfer(transferID string, asset string, transfers []transfer.Hedera, fetch func() (*mirror_node.Response, error), save func(transactionID, scheduleID, status string) error) { + m.Called(transferID, asset, transfers, fetch, save) +}