diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 6875534c48..9fb5a0e905 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -7,6 +7,9 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). - Update default pubnet captive core configuration to replace Whalestack with Creit Technologies in the quorum set ([5564](https://github.com/stellar/go/pull/5564)). +### Fixed +- Fix the account operations endpoint to include InvokeHostFunction operations. The fix ensures that all account operations will be listed going forward. However, it will not retroactively include these operations for previously ingested ledgers; reingesting the historical data is required to address that. ([5574](https://github.com/stellar/go/pull/5574)). + ## 22.0.2 ### Fixed diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index a4dbfdc656..0170e904f5 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -151,7 +151,7 @@ func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors processors.NewOperationProcessor(s.historyQ.NewOperationBatchInsertBuilder(), s.config.NetworkPassphrase), tradeProcessor, processors.NewParticipantsProcessor(accountLoader, - s.historyQ.NewTransactionParticipantsBatchInsertBuilder(), s.historyQ.NewOperationParticipantBatchInsertBuilder()), + s.historyQ.NewTransactionParticipantsBatchInsertBuilder(), s.historyQ.NewOperationParticipantBatchInsertBuilder(), s.config.NetworkPassphrase), processors.NewTransactionProcessor(s.historyQ.NewTransactionBatchInsertBuilder(), s.config.SkipTxmeta), processors.NewClaimableBalancesTransactionProcessor(cbLoader, s.historyQ.NewTransactionClaimableBalanceBatchInsertBuilder(), s.historyQ.NewOperationClaimableBalanceBatchInsertBuilder()), diff --git a/services/horizon/internal/ingest/processors/operations_processor.go b/services/horizon/internal/ingest/processors/operations_processor.go index 39d09b1608..301ec7165a 100644 --- a/services/horizon/internal/ingest/processors/operations_processor.go +++ b/services/horizon/internal/ingest/processors/operations_processor.go @@ -1047,7 +1047,31 @@ func (operation *transactionOperationWrapper) Participants() ([]xdr.AccountId, e case xdr.OperationTypeLiquidityPoolWithdraw: // the only direct participant is the source_account case xdr.OperationTypeInvokeHostFunction: - // the only direct participant is the source_account + if changes, err := operation.transaction.GetOperationChanges(operation.index); err != nil { + return participants, err + } else { + for _, change := range changes { + var data xdr.LedgerEntryData + switch { + case change.Post != nil: + data = change.Post.Data + case change.Pre != nil: + data = change.Pre.Data + default: + log.Errorf("Change Type %s with no pre or post", change.Type.String()) + continue + } + if ledgerKey, err := data.LedgerKey(); err == nil { + participants = append(participants, getLedgerKeyParticipants(ledgerKey)...) + } + } + } + if diagnosticEvents, err := operation.transaction.GetDiagnosticEvents(); err != nil { + return participants, err + } else { + participants = append(participants, getParticipantsFromSACEvents(filterEvents(diagnosticEvents), operation.network)...) + } + case xdr.OperationTypeExtendFootprintTtl: // the only direct participant is the source_account case xdr.OperationTypeRestoreFootprint: @@ -1067,6 +1091,48 @@ func (operation *transactionOperationWrapper) Participants() ([]xdr.AccountId, e return dedupeParticipants(participants), nil } +func getParticipantsFromSACEvents(contractEvents []xdr.ContractEvent, network string) []xdr.AccountId { + var participants []xdr.AccountId + + for _, contractEvent := range contractEvents { + if sacEvent, err := contractevents.NewStellarAssetContractEvent(&contractEvent, network); err == nil { + // 'to' and 'from' fields in the events can be either a Contract address or an Account address. We're + // only interested in account addresses and will skip Contract addresses. + switch sacEvent.GetType() { + case contractevents.EventTypeTransfer: + var transferEvt *contractevents.TransferEvent + transferEvt = sacEvent.(*contractevents.TransferEvent) + var from, to xdr.AccountId + if from, err = xdr.AddressToAccountId(transferEvt.From); err == nil { + participants = append(participants, from) + } + if to, err = xdr.AddressToAccountId(transferEvt.To); err == nil { + participants = append(participants, to) + } + case contractevents.EventTypeMint: + mintEvt := sacEvent.(*contractevents.MintEvent) + var to xdr.AccountId + if to, err = xdr.AddressToAccountId(mintEvt.To); err == nil { + participants = append(participants, to) + } + case contractevents.EventTypeClawback: + clawbackEvt := sacEvent.(*contractevents.ClawbackEvent) + var from xdr.AccountId + if from, err = xdr.AddressToAccountId(clawbackEvt.From); err == nil { + participants = append(participants, from) + } + case contractevents.EventTypeBurn: + burnEvt := sacEvent.(*contractevents.BurnEvent) + var from xdr.AccountId + if from, err = xdr.AddressToAccountId(burnEvt.From); err == nil { + participants = append(participants, from) + } + } + } + } + return participants +} + // dedupeParticipants remove any duplicate ids from `in` func dedupeParticipants(in []xdr.AccountId) []xdr.AccountId { if len(in) <= 1 { @@ -1090,7 +1156,7 @@ func dedupeParticipants(in []xdr.AccountId) []xdr.AccountId { } // OperationsParticipants returns a map with all participants per operation -func operationsParticipants(transaction ingest.LedgerTransaction, sequence uint32) (map[int64][]xdr.AccountId, error) { +func operationsParticipants(transaction ingest.LedgerTransaction, sequence uint32, network string) (map[int64][]xdr.AccountId, error) { participants := map[int64][]xdr.AccountId{} for opi, op := range transaction.Envelope.Operations() { @@ -1099,6 +1165,7 @@ func operationsParticipants(transaction ingest.LedgerTransaction, sequence uint3 transaction: transaction, operation: op, ledgerSequence: sequence, + network: network, } p, err := operation.Participants() diff --git a/services/horizon/internal/ingest/processors/participants_processor.go b/services/horizon/internal/ingest/processors/participants_processor.go index e889e747c0..e97fe91600 100644 --- a/services/horizon/internal/ingest/processors/participants_processor.go +++ b/services/horizon/internal/ingest/processors/participants_processor.go @@ -19,17 +19,21 @@ type ParticipantsProcessor struct { accountLoader *history.AccountLoader txBatch history.TransactionParticipantsBatchInsertBuilder opBatch history.OperationParticipantBatchInsertBuilder + network string } func NewParticipantsProcessor( accountLoader *history.AccountLoader, txBatch history.TransactionParticipantsBatchInsertBuilder, opBatch history.OperationParticipantBatchInsertBuilder, + network string, + ) *ParticipantsProcessor { return &ParticipantsProcessor{ accountLoader: accountLoader, txBatch: txBatch, opBatch: opBatch, + network: network, } } @@ -129,7 +133,7 @@ func (p *ParticipantsProcessor) addOperationsParticipants( sequence uint32, transaction ingest.LedgerTransaction, ) error { - participants, err := operationsParticipants(transaction, sequence) + participants, err := operationsParticipants(transaction, sequence, p.network) if err != nil { return errors.Wrap(err, "could not determine operation participants") } diff --git a/services/horizon/internal/ingest/processors/participants_processor_test.go b/services/horizon/internal/ingest/processors/participants_processor_test.go index f6154b2b39..53c211b7a7 100644 --- a/services/horizon/internal/ingest/processors/participants_processor_test.go +++ b/services/horizon/internal/ingest/processors/participants_processor_test.go @@ -96,6 +96,7 @@ func (s *ParticipantsProcessorTestSuiteLedger) SetupTest() { s.accountLoader, s.mockBatchInsertBuilder, s.mockOperationsBatchInsertBuilder, + networkPassphrase, ) s.txs = []ingest.LedgerTransaction{ diff --git a/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go b/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go index 78afaa6e71..bfd0e65e02 100644 --- a/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go +++ b/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go @@ -3,12 +3,16 @@ package processors import ( + "math/big" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/stellar/go/keypair" "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/strkey" + "github.com/stellar/go/support/contractevents" "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" @@ -1104,7 +1108,7 @@ func TestOperationParticipants(t *testing.T) { xdr.MustAddress("GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD"), xdr.MustAddress("GACAR2AEYEKITE2LKI5RMXF5MIVZ6Q7XILROGDT22O7JX4DSWFS7FDDP"), } - participantsMap, err := operationsParticipants(transaction, sequence) + participantsMap, err := operationsParticipants(transaction, sequence, networkPassphrase) tt.NoError(err) tt.Len(participantsMap, 1) for k, v := range participantsMap { @@ -2285,3 +2289,110 @@ func TestDetailsCoversAllOperationTypes(t *testing.T) { _, err := operation.Details() assert.ErrorContains(t, err, "unknown operation type: ") } + +func TestTestInvokeHostFnOperationParticipants(t *testing.T) { + sourceAddress := "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY" + source := xdr.MustMuxedAddress(sourceAddress) + + randomIssuer := keypair.MustRandom() + randomAsset := xdr.MustNewCreditAsset("TESTING", randomIssuer.Address()) + passphrase := "passphrase" + randomAccount := keypair.MustRandom().Address() + + burnEvtAcc := keypair.MustRandom().Address() + mintEvtAcc := keypair.MustRandom().Address() + clawbkEvtAcc := keypair.MustRandom().Address() + transferEvtFromAcc := keypair.MustRandom().Address() + transferEvtToAcc := keypair.MustRandom().Address() + + transferContractEvent := contractevents.GenerateEvent(contractevents.EventTypeTransfer, transferEvtFromAcc, transferEvtToAcc, "", randomAsset, big.NewInt(10000000), passphrase) + mintContractEvent := contractevents.GenerateEvent(contractevents.EventTypeMint, "", mintEvtAcc, randomAccount, randomAsset, big.NewInt(10000000), passphrase) + burnContractEvent := contractevents.GenerateEvent(contractevents.EventTypeBurn, burnEvtAcc, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase) + clawbackContractEvent := contractevents.GenerateEvent(contractevents.EventTypeClawback, clawbkEvtAcc, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase) + + tx1 := ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 3, + V3: &xdr.TransactionMetaV3{ + SorobanMeta: &xdr.SorobanTransactionMeta{ + Events: []xdr.ContractEvent{ + transferContractEvent, + burnContractEvent, + mintContractEvent, + clawbackContractEvent, + }, + }, + }, + }, + } + + wrapper1 := transactionOperationWrapper{ + transaction: tx1, + operation: xdr.Operation{ + SourceAccount: &source, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + }, + }, + network: passphrase, + } + + participants, err := wrapper1.Participants() + assert.NoError(t, err) + assert.ElementsMatch(t, + []xdr.AccountId{ + xdr.MustAddress(source.Address()), + xdr.MustAddress(mintEvtAcc), + xdr.MustAddress(burnEvtAcc), + xdr.MustAddress(clawbkEvtAcc), + xdr.MustAddress(transferEvtFromAcc), + xdr.MustAddress(transferEvtToAcc), + }, + participants, + ) + + contractId := [32]byte{} + zeroContractStrKey, err := strkey.Encode(strkey.VersionByteContract, contractId[:]) + assert.NoError(t, err) + + transferContractEvent = contractevents.GenerateEvent(contractevents.EventTypeTransfer, zeroContractStrKey, zeroContractStrKey, "", randomAsset, big.NewInt(10000000), passphrase) + mintContractEvent = contractevents.GenerateEvent(contractevents.EventTypeMint, "", zeroContractStrKey, randomAccount, randomAsset, big.NewInt(10000000), passphrase) + burnContractEvent = contractevents.GenerateEvent(contractevents.EventTypeBurn, zeroContractStrKey, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase) + clawbackContractEvent = contractevents.GenerateEvent(contractevents.EventTypeClawback, zeroContractStrKey, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase) + + tx2 := ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 3, + V3: &xdr.TransactionMetaV3{ + SorobanMeta: &xdr.SorobanTransactionMeta{ + Events: []xdr.ContractEvent{ + transferContractEvent, + burnContractEvent, + mintContractEvent, + clawbackContractEvent, + }, + }, + }, + }, + } + + wrapper2 := transactionOperationWrapper{ + transaction: tx2, + operation: xdr.Operation{ + SourceAccount: &source, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + }, + }, + network: passphrase, + } + + participants, err = wrapper2.Participants() + assert.NoError(t, err) + assert.ElementsMatch(t, + []xdr.AccountId{ + xdr.MustAddress(source.Address()), + }, + participants, + ) +} diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index be06fe32f3..19f27d47ac 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -59,7 +59,7 @@ func TestContractMintToAccount(t *testing.T) { itest.Master(), mint(itest, issuer, asset, "20", accountAddressParam(recipient.GetAccountID())), ) - + assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), "", recipientKp.Address(), "20.0000000") assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20")) assertAssetStats(itest, assetStats{ code: code, @@ -92,6 +92,7 @@ func TestContractMintToAccount(t *testing.T) { itest.Master(), transfer(itest, issuer, asset, "30", accountAddressParam(otherRecipient.GetAccountID())), ) + assertAccountInvokeHostFunctionOperation(itest, otherRecipientKp.Address(), issuer, otherRecipientKp.Address(), "30.0000000") assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20")) assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30")) @@ -552,7 +553,8 @@ func TestContractTransferBetweenAccounts(t *testing.T) { recipientKp, transfer(itest, recipientKp.Address(), asset, "30", accountAddressParam(otherRecipient.GetAccountID())), ) - + assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), recipientKp.Address(), otherRecipientKp.Address(), "30.0000000") + assertAccountInvokeHostFunctionOperation(itest, otherRecipientKp.Address(), recipientKp.Address(), otherRecipientKp.Address(), "30.0000000") assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970")) assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30")) @@ -646,6 +648,7 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { recipientKp, transfer(itest, recipientKp.Address(), asset, "30", contractAddressParam(recipientContractID)), ) + assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), recipientKp.Address(), strkeyRecipientContractID, "30.0000000") assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970")) assertContainsEffect(t, getTxEffects(itest, transferTx, asset), effects.EffectAccountDebited, effects.EffectContractCredited) @@ -668,6 +671,7 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { recipientKp, transferFromContract(itest, recipientKp.Address(), asset, recipientContractID, recipientContractHash, "500", accountAddressParam(recipient.GetAccountID())), ) + assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), strkeyRecipientContractID, recipientKp.Address(), "500.0000000") assertContainsEffect(t, getTxEffects(itest, transferTx, asset), effects.EffectContractDebited, effects.EffectAccountCredited) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1470")) @@ -827,6 +831,7 @@ func TestContractBurnFromAccount(t *testing.T) { recipientKp, burn(itest, recipientKp.Address(), asset, "500"), ) + assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), recipientKp.Address(), "", "500.0000000") fx := getTxEffects(itest, burnTx, asset) require.Len(t, fx, 1) @@ -981,6 +986,7 @@ func TestContractClawbackFromAccount(t *testing.T) { itest.Master(), clawback(itest, issuer, asset, "1000", accountAddressParam(recipientKp.Address())), ) + assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), recipientKp.Address(), "", "1000.0000000") assertContainsEffect(t, getTxEffects(itest, clawTx, asset), effects.EffectAccountDebited) assertContainsBalance(itest, recipientKp, issuer, code, 0) @@ -1164,6 +1170,23 @@ func getTxEffects(itest *integration.Test, txHash string, asset xdr.Asset) []eff return result } +func assertAccountInvokeHostFunctionOperation(itest *integration.Test, account string, from string, to string, amount string) { + ops, err := itest.Client().Operations(horizonclient.OperationRequest{ + ForAccount: account, + Limit: 1, + Order: "desc", + }) + + assert.NoError(itest.CurrentTest(), err) + result := ops.Embedded.Records[0] + assert.Equal(itest.CurrentTest(), result.GetType(), operations.TypeNames[xdr.OperationTypeInvokeHostFunction]) + invokeHostFn := result.(operations.InvokeHostFunction) + assert.Equal(itest.CurrentTest(), invokeHostFn.Function, "HostFunctionTypeHostFunctionTypeInvokeContract") + assert.Equal(itest.CurrentTest(), to, invokeHostFn.AssetBalanceChanges[0].To) + assert.Equal(itest.CurrentTest(), from, invokeHostFn.AssetBalanceChanges[0].From) + assert.Equal(itest.CurrentTest(), amount, invokeHostFn.AssetBalanceChanges[0].Amount) +} + func assertEventPayments(itest *integration.Test, txHash string, asset xdr.Asset, from string, to string, evtType string, amount string) { ops, err := itest.Client().Operations(horizonclient.OperationRequest{ ForTransaction: txHash,