diff --git a/rpc/handlers.go b/rpc/handlers.go index 460658886c..a91c97079c 100644 --- a/rpc/handlers.go +++ b/rpc/handlers.go @@ -337,6 +337,7 @@ func (h *Handler) MethodsV0_10() ([]jsonrpc.Method, string) { Params: []jsonrpc.Parameter{ {Name: "finality_status", Optional: true}, {Name: "sender_address", Optional: true}, + {Name: "tags", Optional: true}, }, Handler: h.rpcv10Handler.SubscribeNewTransactions, }, diff --git a/rpc/v10/block_test.go b/rpc/v10/block_test.go index 0b4867d08a..9124c610a2 100644 --- a/rpc/v10/block_test.go +++ b/rpc/v10/block_test.go @@ -15,6 +15,7 @@ import ( rpcv10 "github.com/NethermindEth/juno/rpc/v10" rpcv6 "github.com/NethermindEth/juno/rpc/v6" rpcv9 "github.com/NethermindEth/juno/rpc/v9" + adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" "github.com/NethermindEth/juno/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -890,3 +891,201 @@ func nilToOne(f *felt.Felt) *felt.Felt { } return f } + +func TestBlockWithTxsWithResponseFlags(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + network := &utils.Sepolia + client := feeder.NewTestClient(t, network) + gw := adaptfeeder.New(client) + + block, err := gw.BlockByNumber(t.Context(), 4072139) + require.NoError(t, err) + require.NotNil(t, block) + require.Greater(t, len(block.Transactions), 0) + + // Count invoke v3 transactions with proof_facts and total invoke v3 transactions + var invokeV3WithProofFactsCount int + var invokeV3Count int + for _, tx := range block.Transactions { + if invokeTx, ok := tx.(*core.InvokeTransaction); ok { + if invokeTx.Version != nil && invokeTx.Version.Is(3) { + invokeV3Count++ + if invokeTx.ProofFacts != nil { + invokeV3WithProofFactsCount++ + } + } + } + } + require.Greater( + t, + invokeV3WithProofFactsCount, + 0, + "Block should contain at least one invoke v3 transaction with proof_facts", + ) + + mockReader := mocks.NewMockReader(mockCtrl) + mockSyncReader := mocks.NewMockSyncReader(mockCtrl) + + blockID := rpcv9.BlockIDFromNumber(block.Header.Number) + mockReader.EXPECT().BlockByNumber(block.Header.Number).Return(block, nil).AnyTimes() + mockReader.EXPECT().Network().Return(network).AnyTimes() + mockReader.EXPECT().L1Head().Return(core.L1Head{}, nil).AnyTimes() + + mockReader.EXPECT().BlockCommitmentsByNumber(block.Header.Number).Return(&core.BlockCommitments{ + TransactionCommitment: &felt.Zero, + EventCommitment: &felt.Zero, + ReceiptCommitment: &felt.Zero, + StateDiffCommitment: &felt.Zero, + }, nil).AnyTimes() + mockReader.EXPECT().StateUpdateByNumber(block.Header.Number).Return(&core.StateUpdate{ + StateDiff: &core.StateDiff{}, + }, nil).AnyTimes() + + handler := rpcv10.New(mockReader, mockSyncReader, nil, utils.NewNopZapLogger()) + + t.Run("WithResponseFlag", func(t *testing.T) { + responseFlags := rpcv10.ResponseFlags{IncludeProofFacts: true} + blockWithTxs, rpcErr := handler.BlockWithTxs(&blockID, responseFlags) + require.Nil(t, rpcErr) + require.NotNil(t, blockWithTxs) + + // Verify total number of transactions is the same + require.Equal(t, len(block.Transactions), len(blockWithTxs.Transactions)) + + // Count transactions with proof_facts in response + var txsWithProofFactsCount int + for _, tx := range blockWithTxs.Transactions { + if tx.ProofFacts != nil { + txsWithProofFactsCount++ + } + } + + // Verify number of transactions with proof_facts matches expected + require.Equal( + t, + invokeV3WithProofFactsCount, + txsWithProofFactsCount, + "Number of transactions with proof_facts should match", + ) + }) + + t.Run("WithoutResponseFlag", func(t *testing.T) { + blockWithTxs, rpcErr := handler.BlockWithTxs(&blockID, rpcv10.ResponseFlags{}) + require.Nil(t, rpcErr) + require.NotNil(t, blockWithTxs) + + // Verify total number of transactions is the same + require.Equal(t, len(block.Transactions), len(blockWithTxs.Transactions)) + + // Verify no transactions have proof_facts when flag is not set + for _, tx := range blockWithTxs.Transactions { + require.Nil(t, tx.ProofFacts, "proof_facts should not be included when flag is not set") + } + }) +} + +func TestBlockWithReceiptsWithResponseFlags(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + network := &utils.Sepolia + client := feeder.NewTestClient(t, network) + gw := adaptfeeder.New(client) + + block, err := gw.BlockByNumber(t.Context(), 4072139) + require.NoError(t, err) + require.NotNil(t, block) + require.Greater(t, len(block.Transactions), 0) + require.Equal( + t, + len(block.Transactions), + len(block.Receipts), + "Block should have receipts for all transactions", + ) + + // Count invoke v3 transactions with proof_facts + var invokeV3WithProofFactsCount int + for _, tx := range block.Transactions { + if invokeTx, ok := tx.(*core.InvokeTransaction); ok { + if invokeTx.Version != nil && invokeTx.Version.Is(3) && invokeTx.ProofFacts != nil { + invokeV3WithProofFactsCount++ + } + } + } + require.Greater( + t, + invokeV3WithProofFactsCount, 0, + "Block should contain at least one invoke v3 transaction with proof_facts", + ) + + // Count all transactions + totalTxCount := len(block.Transactions) + + mockReader := mocks.NewMockReader(mockCtrl) + mockSyncReader := mocks.NewMockSyncReader(mockCtrl) + + blockID := rpcv9.BlockIDFromNumber(block.Header.Number) + mockReader.EXPECT().BlockByNumber(block.Header.Number).Return(block, nil).AnyTimes() + mockReader.EXPECT().Network().Return(network).AnyTimes() + mockReader.EXPECT().L1Head().Return(core.L1Head{}, nil).AnyTimes() + + mockReader.EXPECT().BlockCommitmentsByNumber(block.Header.Number).Return(&core.BlockCommitments{ + TransactionCommitment: &felt.Zero, + EventCommitment: &felt.Zero, + ReceiptCommitment: &felt.Zero, + StateDiffCommitment: &felt.Zero, + }, nil).AnyTimes() + mockReader.EXPECT().StateUpdateByNumber(block.Header.Number).Return(&core.StateUpdate{ + StateDiff: &core.StateDiff{}, + }, nil).AnyTimes() + + handler := rpcv10.New(mockReader, mockSyncReader, nil, utils.NewNopZapLogger()) + + t.Run("WithResponseFlag", func(t *testing.T) { + responseFlags := rpcv10.ResponseFlags{IncludeProofFacts: true} + blockWithReceipts, rpcErr := handler.BlockWithReceipts(&blockID, responseFlags) + require.Nil(t, rpcErr) + require.NotNil(t, blockWithReceipts) + + // Verify total number of transactions is the same + require.Equal(t, totalTxCount, len(blockWithReceipts.Transactions)) + + // Count transactions with proof_facts in response + var txsWithProofFactsCount int + for _, txWithReceipt := range blockWithReceipts.Transactions { + if txWithReceipt.Transaction.ProofFacts != nil { + txsWithProofFactsCount++ + } + } + + // Verify number of transactions with proof_facts matches expected + require.Equal( + t, + invokeV3WithProofFactsCount, + txsWithProofFactsCount, + "Number of transactions with proof_facts should match", + ) + }) + + t.Run("WithoutResponseFlag", func(t *testing.T) { + t.Run("WithoutResponseFlag", func(t *testing.T) { + blockWithReceipts, rpcErr := handler.BlockWithReceipts(&blockID, rpcv10.ResponseFlags{}) + require.Nil(t, rpcErr) + require.NotNil(t, blockWithReceipts) + + // Verify total number of transactions is the same + require.Equal(t, len(block.Transactions), len(blockWithReceipts.Transactions)) + + // Verify no transactions have proof_facts when flag is not set + for _, tx := range blockWithReceipts.Transactions { + require.Nil( + t, + tx.Transaction.ProofFacts, + "proof_facts should not be included when flag is not set", + ) + } + }) + }) +} diff --git a/rpc/v10/response_flags.go b/rpc/v10/response_flags.go index 197444cdc6..25444e7c99 100644 --- a/rpc/v10/response_flags.go +++ b/rpc/v10/response_flags.go @@ -27,3 +27,27 @@ func (r *ResponseFlags) UnmarshalJSON(data []byte) error { return nil } + +type SubscriptionTags struct { + IncludeProofFacts bool +} + +func (r *SubscriptionTags) UnmarshalJSON(data []byte) error { + var flags []string + if err := json.Unmarshal(data, &flags); err != nil { + return err + } + + *r = SubscriptionTags{} + + for _, flag := range flags { + switch flag { + case "INCLUDE_PROOF_FACTS": + r.IncludeProofFacts = true + default: + return fmt.Errorf("unknown flag: %s", flag) + } + } + + return nil +} diff --git a/rpc/v10/response_flags_test.go b/rpc/v10/response_flags_test.go index 73696282f9..bae0b6ddaa 100644 --- a/rpc/v10/response_flags_test.go +++ b/rpc/v10/response_flags_test.go @@ -56,3 +56,57 @@ func TestResponseFlags_UnmarshalJSON(t *testing.T) { }) } } + +func TestSubscriptionTags_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + expected rpcv10.SubscriptionTags + expectedError string + }{ + { + name: "empty array", + json: `[]`, + expected: rpcv10.SubscriptionTags{IncludeProofFacts: false}, + }, + { + name: "array with INCLUDE_PROOF_FACTS", + json: `["INCLUDE_PROOF_FACTS"]`, + expected: rpcv10.SubscriptionTags{IncludeProofFacts: true}, + }, + { + name: "array with unknown flag and valid flag", + json: `["INCLUDE_PROOF_FACTS", "UNKNOWN_FLAG"]`, + expectedError: "unknown flag: UNKNOWN_FLAG", + }, + { + name: "case sensitive", + json: `["include_proof_facts"]`, + expectedError: "unknown flag: include_proof_facts", + }, + { + name: "invalid JSON", + json: `{"not": "an array"}`, + expectedError: "cannot unmarshal", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var tags rpcv10.SubscriptionTags + err := json.Unmarshal([]byte(tt.json), &tags) + + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expected, tags) + }) + } +} diff --git a/rpc/v10/subscriptions.go b/rpc/v10/subscriptions.go index 6f8af9fbfe..217e2cf747 100644 --- a/rpc/v10/subscriptions.go +++ b/rpc/v10/subscriptions.go @@ -1022,7 +1022,10 @@ func (h *Handler) SubscribeNewTransactions( ctx context.Context, finalityStatus []rpcv9.TxnStatusWithoutL1, senderAddr []felt.Address, + tags SubscriptionTags, ) (SubscriptionID, *jsonrpc.Error) { + includeProofFacts := tags.IncludeProofFacts + w, ok := jsonrpc.ConnFromContext(ctx) if !ok { return "", jsonrpc.Err(jsonrpc.MethodNotFound, nil) @@ -1057,6 +1060,7 @@ func (h *Handler) SubscribeNewTransactions( head, sentCache, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + includeProofFacts, ) }, onPreLatest: func( @@ -1075,6 +1079,7 @@ func (h *Handler) SubscribeNewTransactions( preLatest.Block, sentCache, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + includeProofFacts, ) }, onPendingData: func( @@ -1097,6 +1102,7 @@ func (h *Handler) SubscribeNewTransactions( pending.GetBlock(), sentCache, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + includeProofFacts, ) } @@ -1109,6 +1115,7 @@ func (h *Handler) SubscribeNewTransactions( pending.GetBlock(), sentCache, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + includeProofFacts, ) if err != nil { return err @@ -1116,7 +1123,7 @@ func (h *Handler) SubscribeNewTransactions( } if slices.Contains(finalityStatus, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusCandidate)) { - return processCandidateTransactions(id, w, senderAddr, pending, sentCache) + return processCandidateTransactions(id, w, senderAddr, pending, sentCache, includeProofFacts) } } return nil @@ -1125,6 +1132,11 @@ func (h *Handler) SubscribeNewTransactions( return h.subscribe(ctx, w, subscriber) } +type SubscriptionNewTransaction struct { + Transaction + FinalityStatus rpcv9.TxnStatusWithoutL1 `json:"finality_status"` +} + // processBlockTransactions streams given block transactions without duplicates func processBlockTransactions( id string, @@ -1133,6 +1145,7 @@ func processBlockTransactions( b *core.Block, sentCache *rpccore.SubscriptionCache[felt.TransactionHash, rpcv9.TxnStatusWithoutL1], status rpcv9.TxnStatusWithoutL1, + includeProofFacts bool, ) error { for _, txn := range b.Transactions { if !filterTxBySender(txn, senderAddr) { @@ -1146,6 +1159,7 @@ func processBlockTransactions( txn, status, id, + includeProofFacts, ); err != nil { return err } @@ -1160,6 +1174,7 @@ func processCandidateTransactions( senderAddr []felt.Address, preConfirmed core.PendingData, sentCache *rpccore.SubscriptionCache[felt.TransactionHash, rpcv9.TxnStatusWithoutL1], + includeProofFacts bool, ) error { for _, txn := range preConfirmed.GetCandidateTransaction() { if !filterTxBySender(txn, senderAddr) { @@ -1173,6 +1188,7 @@ func processCandidateTransactions( txn, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusCandidate), id, + includeProofFacts, ); err != nil { return err } @@ -1189,6 +1205,7 @@ func sendTransactionWithoutDuplicate( txn core.Transaction, finalityStatus rpcv9.TxnStatusWithoutL1, id string, + includeProofFacts bool, ) error { txHash := felt.TransactionHash(*txn.Hash()) if !sentCache.ShouldSend( @@ -1202,8 +1219,8 @@ func sendTransactionWithoutDuplicate( // Add to cache sentCache.Put(blockNumber, &txHash, &finalityStatus) - response := rpcv9.SubscriptionNewTransaction{ - Transaction: *rpcv9.AdaptTransaction(txn), + response := SubscriptionNewTransaction{ + Transaction: AdaptTransaction(txn, includeProofFacts), FinalityStatus: finalityStatus, } @@ -1211,7 +1228,7 @@ func sendTransactionWithoutDuplicate( } // sendTransaction creates a response and sends it to the client -func sendTransaction(w jsonrpc.Conn, result *rpcv9.SubscriptionNewTransaction, id string) error { +func sendTransaction(w jsonrpc.Conn, result *SubscriptionNewTransaction, id string) error { return sendResponse("starknet_subscriptionNewTransaction", w, id, result) } diff --git a/rpc/v10/subscriptions_test.go b/rpc/v10/subscriptions_test.go index dccde950c4..202ab526c4 100644 --- a/rpc/v10/subscriptions_test.go +++ b/rpc/v10/subscriptions_test.go @@ -1682,11 +1682,12 @@ func TestSubscribeNewTransactions(t *testing.T) { toTransactionsWithFinalityStatus := func( txs []core.Transaction, finalityStatus rpcv9.TxnStatusWithoutL1, - ) []*rpcv9.SubscriptionNewTransaction { - txsWithStatus := make([]*rpcv9.SubscriptionNewTransaction, len(txs)) + includeProofFacts bool, + ) []*SubscriptionNewTransaction { + txsWithStatus := make([]*SubscriptionNewTransaction, len(txs)) for i, txn := range txs { - txsWithStatus[i] = &rpcv9.SubscriptionNewTransaction{ - Transaction: *rpcv9.AdaptTransaction(txn), + txsWithStatus[i] = &SubscriptionNewTransaction{ + Transaction: AdaptTransaction(txn, includeProofFacts), FinalityStatus: finalityStatus, } } @@ -1718,7 +1719,7 @@ func TestSubscribeNewTransactions(t *testing.T) { type stepInfo struct { description string notify func() - expect [][]*rpcv9.SubscriptionNewTransaction + expect [][]*SubscriptionNewTransaction expectedReorg *rpcv9.ReorgEvent } @@ -1726,6 +1727,7 @@ func TestSubscribeNewTransactions(t *testing.T) { description string statuses []rpcv9.TxnStatusWithoutL1 senderAddress []felt.Address + tags SubscriptionTags steps []stepInfo } @@ -1739,10 +1741,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead1) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( newHead1.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + false, ), }, }, @@ -1751,10 +1754,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&pending) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( pending.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + false, ), }, }, @@ -1763,10 +1767,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead2) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( newHead2.Transactions[pendingBlockTxCount:], rpcv9.TxnStatusWithoutL1(rpcv9.TxnAcceptedOnL2), + false, ), }, }, @@ -1784,10 +1789,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead1) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( newHead1.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + false, ), }, }, @@ -1796,17 +1802,18 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b2PreConfirmedPartial) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, { description: "pre_confirmed becomes pre_latest", notify: func() { syncer.preLatest.Send(&b2PreLatest) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreLatest.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnAcceptedOnL2), + false, ), }, }, @@ -1815,7 +1822,7 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead2) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, }, } @@ -1830,10 +1837,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b1PreConfirmedPartial) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b1PreConfirmedPartial.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), }, }, @@ -1842,10 +1850,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b1PreConfirmedFull) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b1PreConfirmedFull.Block.Transactions[partialPreConfirmedCount:], rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), }, }, @@ -1854,17 +1863,18 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.preLatest.Send(&b1PreLatest) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, { description: "on new pre_confirmed", notify: func() { syncer.pendingData.Send(&b2PreConfirmedPartial) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreConfirmedPartial.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), }, }, @@ -1873,7 +1883,7 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead1) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, }, } @@ -1888,17 +1898,18 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead1) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, { description: "on new pre_confirmed, only stream CANDIDATES", notify: func() { syncer.pendingData.Send(&b2PreConfirmedPartial) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreConfirmedPartial.CandidateTxs, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusCandidate), + false, ), }, }, @@ -1907,14 +1918,14 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b2PreConfirmedExtended) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, { description: "pre_confirmed become new head do not stream", notify: func() { syncer.newHeads.Send(newHead2) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, }, } @@ -1934,7 +1945,7 @@ func TestSubscribeNewTransactions(t *testing.T) { // notify: func() { // handler.receivedTxFeed.Send(newHead2.Transactions[0]) // }, - // expect: [][]*rpcv9.SubscriptionNewTransaction{ + // expect: [][]*SubscriptionNewTransaction{ // toTransactionsWithFinalityStatus( // newHead2.Transactions[:1], // rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusReceived), @@ -1946,10 +1957,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead1) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( newHead1.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + false, ), }, }, @@ -1958,14 +1970,16 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b2PreConfirmedPartial) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreConfirmedPartial.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), toTransactionsWithFinalityStatus( b2PreConfirmedPartial.CandidateTxs, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusCandidate), + false, ), }, }, @@ -1974,10 +1988,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b2PreConfirmedExtended) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreConfirmedExtended.Block.Transactions[partialPreConfirmedCount:], rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), }, }, @@ -1986,10 +2001,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b2PreConfirmedFull) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreConfirmedFull.Block.Transactions[extendedPreConfirmedCount:], rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), }, }, @@ -1998,10 +2014,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.preLatest.Send(&b2PreLatest) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreLatest.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnAcceptedOnL2), + false, ), }, }, @@ -2010,7 +2027,7 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead2) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, }, } @@ -2039,7 +2056,7 @@ func TestSubscribeNewTransactions(t *testing.T) { // notify: func() { // handler.receivedTxFeed.Send(newHead2.Transactions[0]) // }, - // expect: [][]*rpcv9.SubscriptionNewTransaction{ + // expect: [][]*SubscriptionNewTransaction{ // toTransactionsWithFinalityStatus( // newHead2.Transactions[:1], // rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusReceived), @@ -2051,10 +2068,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(utils.HeapPtr(CreateTestPreConfirmed(t, newHead2, 0))) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( senderTransactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusCandidate), + false, ), }, }, @@ -2063,10 +2081,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b2PreConfirmedFull) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( senderTransactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), }, }, @@ -2075,10 +2094,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead2) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( senderTransactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnAcceptedOnL2), + false, ), }, }, @@ -2095,10 +2115,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.preLatest.Send(&b1PreLatest) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b1PreLatest.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + false, ), }, }, @@ -2107,17 +2128,18 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead1) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, { description: "on new pre-latest block", notify: func() { syncer.preLatest.Send(&b2PreLatest) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreLatest.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + false, ), }, }, @@ -2126,7 +2148,7 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead2) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, }, } @@ -2146,14 +2168,16 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b1PreConfirmedPartial) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b1PreConfirmedPartial.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), toTransactionsWithFinalityStatus( b1PreConfirmedPartial.CandidateTxs, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusCandidate), + false, ), }, }, @@ -2162,10 +2186,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b1PreConfirmedExtended) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b1PreConfirmedExtended.Block.Transactions[partialPreConfirmedCount:], rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), }, }, @@ -2174,10 +2199,11 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.preLatest.Send(&b1PreLatest) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b1PreLatest.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + false, ), }, }, @@ -2186,14 +2212,16 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.pendingData.Send(&b2PreConfirmedPartial) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreConfirmedPartial.Block.Transactions, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), toTransactionsWithFinalityStatus( b2PreConfirmedPartial.CandidateTxs, rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusCandidate), + false, ), }, }, @@ -2202,17 +2230,18 @@ func TestSubscribeNewTransactions(t *testing.T) { notify: func() { syncer.newHeads.Send(newHead1) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{}, + expect: [][]*SubscriptionNewTransaction{}, }, { description: "pre_confirmed update - without duplicates", notify: func() { syncer.pendingData.Send(&b2PreConfirmedExtended) }, - expect: [][]*rpcv9.SubscriptionNewTransaction{ + expect: [][]*SubscriptionNewTransaction{ toTransactionsWithFinalityStatus( b2PreConfirmedExtended.Block.Transactions[partialPreConfirmedCount:], rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusPreConfirmed), + false, ), }, }, @@ -2249,6 +2278,95 @@ func TestSubscribeNewTransactions(t *testing.T) { }, } + // Create a block with Invoke V3 transaction with proof facts for testing + invokeV3WithProofFacts := &core.InvokeTransaction{ + TransactionHash: felt.NewFromUint64[felt.Felt](123), + Version: new(core.TransactionVersion).SetUint64(3), + SenderAddress: felt.NewFromUint64[felt.Felt](456), + Nonce: felt.NewFromUint64[felt.Felt](1), + ResourceBounds: map[core.Resource]core.ResourceBounds{ + core.ResourceL1Gas: { + MaxAmount: 1, + MaxPricePerUnit: felt.NewFromUint64[felt.Felt](10), + }, + core.ResourceL2Gas: { + MaxAmount: 1, + MaxPricePerUnit: felt.NewFromUint64[felt.Felt](10), + }, + }, + ProofFacts: []*felt.Felt{ + felt.NewFromUint64[felt.Felt](999), + }, + } + proofFactsBlock := &core.Block{ + Header: &core.Header{ + Hash: felt.NewFromUint64[felt.Felt](99999), + ParentHash: newHead2.ParentHash, + Number: newHead2.Number + 1, + SequencerAddress: newHead2.SequencerAddress, + TransactionCount: 1, + Timestamp: newHead2.Timestamp + 1, + ProtocolVersion: newHead2.ProtocolVersion, + L1GasPriceETH: newHead2.L1GasPriceETH, + L1GasPriceSTRK: newHead2.L1GasPriceSTRK, + L2GasPrice: newHead2.L2GasPrice, + L1DataGasPrice: newHead2.L1DataGasPrice, + L1DAMode: newHead2.L1DAMode, + }, + Transactions: []core.Transaction{invokeV3WithProofFacts}, + Receipts: []*core.TransactionReceipt{ + { + TransactionHash: invokeV3WithProofFacts.TransactionHash, + Events: []*core.Event{}, + }, + }, + } + proofFactsBlock.EventsBloom = core.EventsBloom(proofFactsBlock.Receipts) + + proofFactsWithTag := testCase{ + description: "Includes proof_facts when INCLUDE_PROOF_FACTS tag is passed", + statuses: nil, + senderAddress: nil, + tags: SubscriptionTags{IncludeProofFacts: true}, + steps: []stepInfo{ + { + description: "on new head with invoke v3 transaction", + notify: func() { + syncer.newHeads.Send(proofFactsBlock) + }, + expect: [][]*SubscriptionNewTransaction{ + toTransactionsWithFinalityStatus( + proofFactsBlock.Transactions, + rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + true, + ), + }, + }, + }, + } + + proofFactsWithoutTag := testCase{ + description: "Omits proof_facts when INCLUDE_PROOF_FACTS tag is not passed", + statuses: nil, + senderAddress: nil, + tags: SubscriptionTags{IncludeProofFacts: false}, + steps: []stepInfo{ + { + description: "on new head with invoke v3 transaction", + notify: func() { + syncer.newHeads.Send(proofFactsBlock) + }, + expect: [][]*SubscriptionNewTransaction{ + toTransactionsWithFinalityStatus( + proofFactsBlock.Transactions, + rpcv9.TxnStatusWithoutL1(rpcv9.TxnStatusAcceptedOnL2), + false, + ), + }, + }, + }, + } + testCases := []testCase{ preStarknet0_14_0DefaultFinality, defaultFinality, // onlyAcceptedOnL2 @@ -2259,11 +2377,18 @@ func TestSubscribeNewTransactions(t *testing.T) { preLatestTransactions, deduplication, reorgEvent, + proofFactsWithTag, + proofFactsWithoutTag, } - //nolint:dupl // Shares similar structure with other tests but tests different method for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { - subID, conn := createTestNewTransactionsWebsocket(t, handler, tc.statuses, tc.senderAddress) + subID, conn := createTestNewTransactionsWebsocket( + t, + handler, + tc.statuses, + tc.senderAddress, + tc.tags, + ) for _, step := range tc.steps { if step.notify != nil { step.notify() @@ -2296,7 +2421,7 @@ func TestSubscribeNewTransactions(t *testing.T) { subCtx := context.WithValue(t.Context(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) - id, rpcErr := handler.SubscribeNewTransactions(subCtx, nil, addresses) + id, rpcErr := handler.SubscribeNewTransactions(subCtx, nil, addresses, SubscriptionTags{}) assert.Zero(t, id) assert.Equal(t, rpccore.ErrTooManyAddressesInFilter, rpcErr) }) @@ -2835,7 +2960,6 @@ func TestSubscribeTransactionReceipts(t *testing.T) { reorgEvent, } - //nolint:dupl // Shares similar structure with other tests but tests different method for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { subID, conn := createTestTransactionReceiptsWebsocket(t, handler, tc.senderAddress, tc.statuses) @@ -3084,7 +3208,7 @@ func assertNextTransactions( t *testing.T, conn net.Conn, id SubscriptionID, - transactions []*rpcv9.SubscriptionNewTransaction, + transactions []*SubscriptionNewTransaction, ) { t.Helper() @@ -3257,12 +3381,16 @@ func createTestTxStatusWebsocket( } func createTestNewTransactionsWebsocket( - t *testing.T, h *Handler, finalityStatus []rpcv9.TxnStatusWithoutL1, senderAddress []felt.Address, + t *testing.T, + h *Handler, + finalityStatus []rpcv9.TxnStatusWithoutL1, + senderAddress []felt.Address, + tags SubscriptionTags, ) (SubscriptionID, net.Conn) { t.Helper() return createTestWebsocket(t, func(ctx context.Context) (SubscriptionID, *jsonrpc.Error) { - return h.SubscribeNewTransactions(ctx, finalityStatus, senderAddress) + return h.SubscribeNewTransactions(ctx, finalityStatus, senderAddress, tags) }) }