diff --git a/data/scresult.go b/data/scresult.go index b246528d..95a71c5c 100644 --- a/data/scresult.go +++ b/data/scresult.go @@ -43,4 +43,5 @@ type ScResult struct { SenderAddressBytes []byte `json:"-"` InitialTxGasUsed uint64 `json:"-"` InitialTxFee string `json:"-"` + GasRefunded uint64 `json:"-"` } diff --git a/data/transaction.go b/data/transaction.go index 76060d3b..0837a590 100644 --- a/data/transaction.go +++ b/data/transaction.go @@ -50,11 +50,11 @@ type Transaction struct { CompletedEvent bool `json:"completedEvent,omitempty"` RelayedAddr string `json:"relayer,omitempty"` RelayedSignature string `json:"relayerSignature,omitempty"` + HadRefund bool `json:"hadRefund,omitempty"` ExecutionOrder int `json:"-"` SmartContractResults []*ScResult `json:"-"` Hash string `json:"-"` BlockHash string `json:"-"` - HadRefund bool `json:"-"` } // Receipt is a structure containing all the fields that need to be safe for a Receipt @@ -89,8 +89,9 @@ type ResponseTransactionDB struct { // FeeData is the structure that contains data about transaction fee and gas used type FeeData struct { - FeeNum float64 - Fee string - GasUsed uint64 - Receiver string + FeeNum float64 + Fee string + GasUsed uint64 + Receiver string + GasRefunded uint64 } diff --git a/go.mod b/go.mod index ceddddaa..7962721f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/gin-contrib/cors v1.4.0 github.com/gin-gonic/gin v1.9.1 github.com/multiversx/mx-chain-communication-go v1.1.0 - github.com/multiversx/mx-chain-core-go v1.2.24-0.20250109151319-81a62c045af8 + github.com/multiversx/mx-chain-core-go v1.2.24-0.20250116081327-adb8c08089b4 github.com/multiversx/mx-chain-logger-go v1.0.15 github.com/multiversx/mx-chain-vm-common-go v1.5.13 github.com/prometheus/client_model v0.4.0 diff --git a/go.sum b/go.sum index f9acb8db..693b9fb2 100644 --- a/go.sum +++ b/go.sum @@ -249,8 +249,8 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/multiversx/mx-chain-communication-go v1.1.0 h1:J7bX6HoN3HiHY7cUeEjG8AJWgQDDPcY+OPDOsSUOkRE= github.com/multiversx/mx-chain-communication-go v1.1.0/go.mod h1:WK6bP4pGEHGDDna/AYRIMtl6G9OA0NByI1Lw8PmOnRM= -github.com/multiversx/mx-chain-core-go v1.2.24-0.20250109151319-81a62c045af8 h1:0ivlwcl+dKK7BTVngm1uNM2aDneaXK2rhS0HVeBkvYg= -github.com/multiversx/mx-chain-core-go v1.2.24-0.20250109151319-81a62c045af8/go.mod h1:B5zU4MFyJezmEzCsAHE9YNULmGCm2zbPHvl9hazNxmE= +github.com/multiversx/mx-chain-core-go v1.2.24-0.20250116081327-adb8c08089b4 h1:rWYFL38q5cbo5MtdW2DvAp4+WMaVp8e7gBjmrLQ9SCY= +github.com/multiversx/mx-chain-core-go v1.2.24-0.20250116081327-adb8c08089b4/go.mod h1:B5zU4MFyJezmEzCsAHE9YNULmGCm2zbPHvl9hazNxmE= github.com/multiversx/mx-chain-crypto-go v1.2.12 h1:zWip7rpUS4CGthJxfKn5MZfMfYPjVjIiCID6uX5BSOk= github.com/multiversx/mx-chain-logger-go v1.0.15 h1:HlNdK8etyJyL9NQ+6mIXyKPEBo+wRqOwi3n+m2QIHXc= github.com/multiversx/mx-chain-logger-go v1.0.15/go.mod h1:t3PRKaWB1M+i6gUfD27KXgzLJJC+mAQiN+FLlL1yoGQ= diff --git a/integrationtests/relayedV3_test.go b/integrationtests/relayedV3_test.go new file mode 100644 index 00000000..d7be24d1 --- /dev/null +++ b/integrationtests/relayedV3_test.go @@ -0,0 +1,139 @@ +//go:build integrationtests + +package integrationtests + +import ( + "context" + "encoding/hex" + "math/big" + "testing" + + dataBlock "github.com/multiversx/mx-chain-core-go/data/block" + "github.com/multiversx/mx-chain-core-go/data/outport" + "github.com/multiversx/mx-chain-core-go/data/smartContractResult" + "github.com/multiversx/mx-chain-core-go/data/transaction" + indexerdata "github.com/multiversx/mx-chain-es-indexer-go/process/dataindexer" + "github.com/stretchr/testify/require" +) + +func TestRelayedV3TransactionWithMultipleRefunds(t *testing.T) { + setLogLevelDebug() + + esClient, err := createESClient(esURL) + require.Nil(t, err) + + esProc, err := CreateElasticProcessor(esClient) + require.Nil(t, err) + + txHash := []byte("relayedTxV3WithMultipleRefunds") + header := &dataBlock.Header{ + Round: 50, + TimeStamp: 5040, + } + + body := &dataBlock.Body{ + MiniBlocks: dataBlock.MiniBlockSlice{ + { + Type: dataBlock.TxBlock, + SenderShardID: 0, + ReceiverShardID: 0, + TxHashes: [][]byte{txHash}, + }, + }, + } + + initialTx := &transaction.Transaction{ + Nonce: 1000, + SndAddr: decodeAddress("erd1ykqd64fxxpp4wsz0v7sjqem038wfpzlljhx4mhwx8w9lcxmdzcfszrp64a"), + RcvAddr: decodeAddress("erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3"), + RelayerAddr: decodeAddress("erd10ksryjr065ad5475jcg82pnjfg9j9qtszjsrp24anl6ym7cmeddshwnru8"), + Signature: []byte("d"), + RelayerSignature: []byte("a"), + GasLimit: 500_000_000, + GasPrice: 1000000000, + Value: big.NewInt(0), + Data: []byte("doSomething"), + } + + txInfo := &outport.TxInfo{ + Transaction: initialTx, + FeeInfo: &outport.FeeInfo{ + GasUsed: 180_150_000, + Fee: big.NewInt(2864760000000000), + InitialPaidFee: big.NewInt(2864760000000000), + }, + ExecutionOrder: 0, + } + + pool := &outport.TransactionPool{ + Transactions: map[string]*outport.TxInfo{ + hex.EncodeToString(txHash): txInfo, + }, + } + err = esProc.SaveTransactions(createOutportBlockWithHeader(body, header, pool, nil, testNumOfShards)) + require.Nil(t, err) + + ids := []string{hex.EncodeToString(txHash)} + genericResponse := &GenericResponse{} + err = esClient.DoMultiGet(context.Background(), ids, indexerdata.TransactionsIndex, true, genericResponse) + require.Nil(t, err) + + require.JSONEq(t, + readExpectedResult("./testdata/relayedTxV3/relayed-v3-no-refund.json"), + string(genericResponse.Docs[0].Source), + ) + + // execute first SCR with refund + pool = &outport.TransactionPool{ + SmartContractResults: map[string]*outport.SCRInfo{ + "scrHash": { + SmartContractResult: &smartContractResult.SmartContractResult{ + OriginalTxHash: txHash, + }, + FeeInfo: &outport.FeeInfo{ + GasRefunded: 9_692_000, + Fee: big.NewInt(96920000000000), + }, + }, + }, + } + err = esProc.SaveTransactions(createOutportBlockWithHeader(body, header, pool, nil, testNumOfShards)) + require.Nil(t, err) + + ids = []string{hex.EncodeToString(txHash)} + genericResponse = &GenericResponse{} + err = esClient.DoMultiGet(context.Background(), ids, indexerdata.TransactionsIndex, true, genericResponse) + require.Nil(t, err) + + require.JSONEq(t, + readExpectedResult("./testdata/relayedTxV3/relayed-v3-with-one-refund.json"), + string(genericResponse.Docs[0].Source), + ) + + // execute second SCR with refund + pool = &outport.TransactionPool{ + SmartContractResults: map[string]*outport.SCRInfo{ + "scrHash": { + SmartContractResult: &smartContractResult.SmartContractResult{ + OriginalTxHash: txHash, + }, + FeeInfo: &outport.FeeInfo{ + GasRefunded: 9_692_000, + Fee: big.NewInt(96920000000000), + }, + }, + }, + } + err = esProc.SaveTransactions(createOutportBlockWithHeader(body, header, pool, nil, testNumOfShards)) + require.Nil(t, err) + + ids = []string{hex.EncodeToString(txHash)} + genericResponse = &GenericResponse{} + err = esClient.DoMultiGet(context.Background(), ids, indexerdata.TransactionsIndex, true, genericResponse) + require.Nil(t, err) + + require.JSONEq(t, + readExpectedResult("./testdata/relayedTxV3/relayed-v3-with-two-refunds.json"), + string(genericResponse.Docs[0].Source), + ) +} diff --git a/integrationtests/testdata/relayedTxV3/relayed-v3-no-refund.json b/integrationtests/testdata/relayedTxV3/relayed-v3-no-refund.json new file mode 100644 index 00000000..e4c03f30 --- /dev/null +++ b/integrationtests/testdata/relayedTxV3/relayed-v3-no-refund.json @@ -0,0 +1,28 @@ +{ + "miniBlockHash": "785a251c08b314939528e553ac879dbee0627fbe2b76c0bca601c3f1e7162640", + "nonce": 1000, + "round": 50, + "value": "0", + "valueNum": 0, + "receiver": "erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3", + "sender": "erd1ykqd64fxxpp4wsz0v7sjqem038wfpzlljhx4mhwx8w9lcxmdzcfszrp64a", + "receiverShard": 0, + "senderShard": 0, + "gasPrice": 1000000000, + "gasLimit": 500000000, + "gasUsed": 180150000, + "fee": "2864760000000000", + "feeNum": 0.00286476, + "initialPaidFee": "2864760000000000", + "data": "ZG9Tb21ldGhpbmc=", + "signature": "64", + "timestamp": 5040, + "status": "success", + "searchOrder": 0, + "isScCall": true, + "operation": "transfer", + "function": "doSomething", + "isRelayed": true, + "relayer": "erd10ksryjr065ad5475jcg82pnjfg9j9qtszjsrp24anl6ym7cmeddshwnru8", + "relayerSignature": "61" +} diff --git a/integrationtests/testdata/relayedTxV3/relayed-v3-with-one-refund.json b/integrationtests/testdata/relayedTxV3/relayed-v3-with-one-refund.json new file mode 100644 index 00000000..d8aee273 --- /dev/null +++ b/integrationtests/testdata/relayedTxV3/relayed-v3-with-one-refund.json @@ -0,0 +1,29 @@ +{ + "miniBlockHash": "785a251c08b314939528e553ac879dbee0627fbe2b76c0bca601c3f1e7162640", + "nonce": 1000, + "round": 50, + "value": "0", + "valueNum": 0, + "receiver": "erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3", + "sender": "erd1ykqd64fxxpp4wsz0v7sjqem038wfpzlljhx4mhwx8w9lcxmdzcfszrp64a", + "receiverShard": 0, + "senderShard": 0, + "gasPrice": 1000000000, + "gasLimit": 500000000, + "gasUsed": 170458000, + "fee": "2767840000000000", + "feeNum": 0.00276784, + "initialPaidFee": "2864760000000000", + "data": "ZG9Tb21ldGhpbmc=", + "signature": "64", + "timestamp": 5040, + "status": "success", + "searchOrder": 0, + "isScCall": true, + "operation": "transfer", + "function": "doSomething", + "isRelayed": true, + "relayer": "erd10ksryjr065ad5475jcg82pnjfg9j9qtszjsrp24anl6ym7cmeddshwnru8", + "relayerSignature": "61", + "hadRefund": true +} diff --git a/integrationtests/testdata/relayedTxV3/relayed-v3-with-two-refunds.json b/integrationtests/testdata/relayedTxV3/relayed-v3-with-two-refunds.json new file mode 100644 index 00000000..b060be66 --- /dev/null +++ b/integrationtests/testdata/relayedTxV3/relayed-v3-with-two-refunds.json @@ -0,0 +1,29 @@ +{ + "miniBlockHash": "785a251c08b314939528e553ac879dbee0627fbe2b76c0bca601c3f1e7162640", + "nonce": 1000, + "round": 50, + "value": "0", + "valueNum": 0, + "receiver": "erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3", + "sender": "erd1ykqd64fxxpp4wsz0v7sjqem038wfpzlljhx4mhwx8w9lcxmdzcfszrp64a", + "receiverShard": 0, + "senderShard": 0, + "gasPrice": 1000000000, + "gasLimit": 500000000, + "gasUsed": 160766000, + "fee": "2670920000000000", + "feeNum": 0.0026709200000000002, + "initialPaidFee": "2864760000000000", + "data": "ZG9Tb21ldGhpbmc=", + "signature": "64", + "timestamp": 5040, + "status": "success", + "searchOrder": 0, + "isScCall": true, + "operation": "transfer", + "function": "doSomething", + "isRelayed": true, + "relayer": "erd10ksryjr065ad5475jcg82pnjfg9j9qtszjsrp24anl6ym7cmeddshwnru8", + "relayerSignature": "61", + "hadRefund": true +} diff --git a/process/elasticproc/transactions/scrsDataToTransactions.go b/process/elasticproc/transactions/scrsDataToTransactions.go index f18aa741..af2a5148 100644 --- a/process/elasticproc/transactions/scrsDataToTransactions.go +++ b/process/elasticproc/transactions/scrsDataToTransactions.go @@ -54,7 +54,7 @@ func (st *scrsDataToTransactions) processTransactionsAfterSCRsWereAttached(trans func (st *scrsDataToTransactions) processSCRsWithoutTx(scrs []*data.ScResult) map[string]*data.FeeData { txHashRefund := make(map[string]*data.FeeData) for _, scr := range scrs { - if scr.InitialTxGasUsed == 0 { + if scr.InitialTxGasUsed == 0 && scr.GasRefunded == 0 { continue } @@ -70,10 +70,11 @@ func (st *scrsDataToTransactions) processSCRsWithoutTx(scrs []*data.ScResult) ma } txHashRefund[scr.OriginalTxHash] = &data.FeeData{ - FeeNum: feeNum, - Fee: scr.InitialTxFee, - GasUsed: scr.InitialTxGasUsed, - Receiver: scr.Receiver, + FeeNum: feeNum, + Fee: scr.InitialTxFee, + GasUsed: scr.InitialTxGasUsed, + Receiver: scr.Receiver, + GasRefunded: scr.GasRefunded, } } diff --git a/process/elasticproc/transactions/serialize.go b/process/elasticproc/transactions/serialize.go index 0c3a8803..4cd6bf3f 100644 --- a/process/elasticproc/transactions/serialize.go +++ b/process/elasticproc/transactions/serialize.go @@ -51,7 +51,35 @@ func (tdp *txsDatabaseProcessor) SerializeReceipts(receipts []*data.Receipt, buf func (tdp *txsDatabaseProcessor) SerializeTransactionsFeeData(txHashRefund map[string]*data.FeeData, buffSlice *data.BufferSlice, index string) error { for txHash, feeData := range txHashRefund { meta := []byte(fmt.Sprintf(`{"update":{ "_index":"%s","_id":"%s"}}%s`, index, converters.JsonEscape(txHash), "\n")) - codeToExecute := ` + + var codeToExecute string + if feeData.GasRefunded != 0 { + codeToExecute = ` + if ('create' == ctx.op) { + ctx.op = 'noop' + } else { + BigInteger feeFromSource; + if ((ctx._source.containsKey('hadRefund')) && (ctx._source.hadRefund)) { + feeFromSource = new BigInteger(ctx._source.fee); + } else { + feeFromSource = new BigInteger(ctx._source.initialPaidFee); + ctx._source.hadRefund = true; + } + + BigInteger fee = new BigInteger(params.fee); + if (feeFromSource.compareTo(fee) > 0) { + ctx._source.fee = feeFromSource.subtract(fee).toString(); + } + if (ctx._source.feeNum > params.feeNum) { + ctx._source.feeNum -= params.feeNum; + } + if (ctx._source.gasUsed > params.gasRefunded) { + ctx._source.gasUsed -= params.gasRefunded; + } + } + ` + } else { + codeToExecute = ` if ('create' == ctx.op) { ctx.op = 'noop' } else { @@ -60,13 +88,14 @@ func (tdp *txsDatabaseProcessor) SerializeTransactionsFeeData(txHashRefund map[s ctx._source.gasUsed = params.gasUsed; } ` + } serializedDataStr := fmt.Sprintf(`{"scripted_upsert": true, "script": {`+ `"source": "%s",`+ `"lang": "painless",`+ - `"params": {"fee": "%s", "gasUsed": %d, "feeNum": %g}},`+ + `"params": {"fee": "%s", "gasUsed": %d, "feeNum": %g, "gasRefunded": %d}},`+ `"upsert": {}}`, - converters.FormatPainlessSource(codeToExecute), feeData.Fee, feeData.GasUsed, feeData.FeeNum, + converters.FormatPainlessSource(codeToExecute), feeData.Fee, feeData.GasUsed, feeData.FeeNum, feeData.GasRefunded, ) err := buffSlice.PutData(meta, []byte(serializedDataStr)) diff --git a/process/elasticproc/transactions/serialize_test.go b/process/elasticproc/transactions/serialize_test.go index 1565ee2d..b4e2bf96 100644 --- a/process/elasticproc/transactions/serialize_test.go +++ b/process/elasticproc/transactions/serialize_test.go @@ -146,7 +146,7 @@ func TestTxsDatabaseProcessor_SerializeTransactionWithRefund(t *testing.T) { require.Nil(t, err) expectedBuff := `{"update":{ "_index":"transactions","_id":"txHash"}} -{"scripted_upsert": true, "script": {"source": "if ('create' == ctx.op) {ctx.op = 'noop'} else {ctx._source.fee = params.fee;ctx._source.feeNum = params.feeNum;ctx._source.gasUsed = params.gasUsed;}","lang": "painless","params": {"fee": "100000", "gasUsed": 5000, "feeNum": 5e-15}},"upsert": {}} +{"scripted_upsert": true, "script": {"source": "if ('create' == ctx.op) {ctx.op = 'noop'} else {ctx._source.fee = params.fee;ctx._source.feeNum = params.feeNum;ctx._source.gasUsed = params.gasUsed;}","lang": "painless","params": {"fee": "100000", "gasUsed": 5000, "feeNum": 5e-15, "gasRefunded": 0}},"upsert": {}} ` require.Equal(t, expectedBuff, buffSlice.Buffers()[0].String()) } diff --git a/process/elasticproc/transactions/smartContractResultsProcessor.go b/process/elasticproc/transactions/smartContractResultsProcessor.go index 2fcb7ccf..ce52062a 100644 --- a/process/elasticproc/transactions/smartContractResultsProcessor.go +++ b/process/elasticproc/transactions/smartContractResultsProcessor.go @@ -189,6 +189,7 @@ func (proc *smartContractResultsProcessor) prepareSmartContractResult( OriginalSender: originalSenderAddr, InitialTxFee: feeInfo.Fee.String(), InitialTxGasUsed: feeInfo.GasUsed, + GasRefunded: feeInfo.GasRefunded, ExecutionOrder: int(scrInfo.ExecutionOrder), } } diff --git a/process/elasticproc/transactions/transactionDBBuilder.go b/process/elasticproc/transactions/transactionDBBuilder.go index 6a522b7a..3aab4884 100644 --- a/process/elasticproc/transactions/transactionDBBuilder.go +++ b/process/elasticproc/transactions/transactionDBBuilder.go @@ -125,12 +125,17 @@ func (dtb *dbTransactionBuilder) prepareTransaction( Operation: res.Operation, RelayedSignature: hex.EncodeToString(tx.RelayerSignature), RelayedAddr: relayedAddress, + HadRefund: feeInfo.HadRefund, } + hasValidRelayer := len(eTx.RelayedAddr) == len(eTx.Sender) && len(eTx.RelayedAddr) > 0 + hasValidRelayerSignature := len(eTx.RelayedSignature) == len(eTx.Signature) && len(eTx.RelayedSignature) > 0 + isRelayedV3 := hasValidRelayer && hasValidRelayerSignature + eTx.Function = converters.TruncateFieldIfExceedsMaxLength(res.Function) eTx.Tokens = converters.TruncateSliceElementsIfExceedsMaxLength(res.Tokens) eTx.ReceiversShardIDs = res.ReceiversShardID - eTx.IsRelayed = res.IsRelayed + eTx.IsRelayed = res.IsRelayed || isRelayedV3 return eTx }