diff --git a/.golangci.yml b/.golangci.yml index c4dbfef2..31a2bd68 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,8 +15,9 @@ linters-settings: values: regexp: COMPANY: .* + YEAR_FUZZY: '\d\d\d\d(,\d\d\d\d)?' template: |- - Copyright © {{ YEAR }} {{ COMPANY }} + Copyright © {{ YEAR_FUZZY }} {{ COMPANY }} SPDX-License-Identifier: Apache-2.0 diff --git a/db/migrations/postgres/000009_add_transactions_status_index.down.sql b/db/migrations/postgres/000009_add_transactions_status_index.down.sql index 9651d311..2d6ee3b0 100644 --- a/db/migrations/postgres/000009_add_transactions_status_index.down.sql +++ b/db/migrations/postgres/000009_add_transactions_status_index.down.sql @@ -1 +1,3 @@ -DROP INDEX IF EXISTS transactions_status; \ No newline at end of file +BEGIN; +DROP INDEX IF EXISTS transactions_status; +COMMIT; \ No newline at end of file diff --git a/db/migrations/postgres/000009_add_transactions_status_index.up.sql b/db/migrations/postgres/000009_add_transactions_status_index.up.sql index 4c57b567..6bad28c1 100644 --- a/db/migrations/postgres/000009_add_transactions_status_index.up.sql +++ b/db/migrations/postgres/000009_add_transactions_status_index.up.sql @@ -1 +1,3 @@ -CREATE INDEX transactions_status ON transactions(status); \ No newline at end of file +BEGIN; +CREATE INDEX transactions_status ON transactions(status); +COMMIT; \ No newline at end of file diff --git a/db/migrations/postgres/000010_add_listeners_type.down.sql b/db/migrations/postgres/000010_add_listeners_type.down.sql new file mode 100644 index 00000000..04d45d43 --- /dev/null +++ b/db/migrations/postgres/000010_add_listeners_type.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +ALTER TABLE listeners DROP COLUMN "type"; + +COMMIT; \ No newline at end of file diff --git a/db/migrations/postgres/000010_add_listeners_type.up.sql b/db/migrations/postgres/000010_add_listeners_type.up.sql new file mode 100644 index 00000000..a431fed2 --- /dev/null +++ b/db/migrations/postgres/000010_add_listeners_type.up.sql @@ -0,0 +1,7 @@ +BEGIN; + +ALTER TABLE listeners ADD COLUMN "type" VARCHAR(64); +UPDATE listeners SET "type" = 'events'; +ALTER TABLE listeners ALTER COLUMN "type" SET NOT NULL; + +COMMIT; \ No newline at end of file diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index a05effa5..81235826 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -45,6 +45,8 @@ type Manager interface { Stop() NewBlockHashes() chan<- *ffcapi.BlockHashEvent CheckInFlight(listenerID *fftypes.UUID) bool + StartConfirmedBlockListener(ctx context.Context, id *fftypes.UUID, fromBlock string, checkpoint *ffcapi.BlockListenerCheckpoint, eventStream chan<- *ffcapi.ListenerEvent) error + StopConfirmedBlockListener(ctx context.Context, id *fftypes.UUID) error } type NotificationType string @@ -99,6 +101,8 @@ type blockConfirmationManager struct { pendingMux sync.Mutex receiptChecker *receiptChecker retry *retry.Retry + cblLock sync.Mutex + cbls map[fftypes.UUID]*confirmedBlockListener fetchReceiptUponEntry bool done chan struct{} } @@ -108,6 +112,7 @@ func NewBlockConfirmationManager(baseContext context.Context, connector ffcapi.A bcm := &blockConfirmationManager{ baseContext: baseContext, connector: connector, + cbls: make(map[fftypes.UUID]*confirmedBlockListener), blockListenerStale: true, requiredConfirmations: config.GetInt(tmconfig.ConfirmationsRequired), staleReceiptTimeout: config.GetDuration(tmconfig.ConfirmationsStaleReceiptTimeout), @@ -233,6 +238,9 @@ func (bcm *blockConfirmationManager) Stop() { // Reset context ready for restart bcm.ctx, bcm.cancelFunc = context.WithCancel(bcm.baseContext) } + for _, cbl := range bcm.copyCBLsList() { + _ = bcm.StopConfirmedBlockListener(bcm.ctx, cbl.id) + } } func (bcm *blockConfirmationManager) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { @@ -301,9 +309,10 @@ func (bcm *blockConfirmationManager) getBlockByHash(blockHash string) (*apitypes return blockInfo, nil } -func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, expectedParentHash string) (*apitypes.BlockInfo, error) { +func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, allowCache bool, expectedParentHash string) (*apitypes.BlockInfo, error) { res, reason, err := bcm.connector.BlockInfoByNumber(bcm.ctx, &ffcapi.BlockInfoByNumberRequest{ BlockNumber: fftypes.NewFFBigInt(int64(blockNumber)), + AllowCache: allowCache, ExpectedParentHash: expectedParentHash, }) if err != nil { @@ -326,6 +335,27 @@ func transformBlockInfo(res *ffcapi.BlockInfo) *apitypes.BlockInfo { } } +func (bcm *blockConfirmationManager) copyCBLsList() []*confirmedBlockListener { + bcm.cblLock.Lock() + defer bcm.cblLock.Unlock() + cbls := make([]*confirmedBlockListener, 0, len(bcm.cbls)) + for _, cbl := range bcm.cbls { + cbls = append(cbls, cbl) + } + return cbls +} + +func (bcm *blockConfirmationManager) propagateBlockHashToCBLs(bhe *ffcapi.BlockHashEvent) { + bcm.cblLock.Lock() + defer bcm.cblLock.Unlock() + for _, cbl := range bcm.cbls { + select { + case cbl.newBlockHashes <- bhe: + case <-cbl.processorDone: + } + } +} + func (bcm *blockConfirmationManager) confirmationsListener() { defer close(bcm.done) notifications := make([]*Notification, 0) @@ -340,6 +370,11 @@ func (bcm *blockConfirmationManager) confirmationsListener() { } blockHashes = append(blockHashes, bhe.BlockHashes...) + // Need to also pass this event to any confirmed block listeners + // (they promise to always be efficient in handling these, having a go-routine + // dedicated to spinning fast just processing those separate to dispatching them) + bcm.propagateBlockHashToCBLs(bhe) + if bhe.Created != nil { for i := 0; i < len(bhe.BlockHashes); i++ { bcm.metricsEmitter.RecordBlockHashQueueingMetrics(bcm.ctx, time.Since(*bhe.Created.Time()).Seconds()) @@ -371,7 +406,7 @@ func (bcm *blockConfirmationManager) confirmationsListener() { if bcm.blockListenerStale { if err := bcm.walkChain(blocks); err != nil { - log.L(bcm.ctx).Errorf("Failed to create walk chain after restoring blockListener: %s", err) + log.L(bcm.ctx).Errorf("Failed to walk chain after restoring blockListener: %s", err) continue } bcm.blockListenerStale = false @@ -704,7 +739,7 @@ func (bs *blockState) getByNumber(blockNumber uint64, expectedParentHash string) if block != nil { return block, nil } - block, err := bs.bcm.getBlockByNumber(blockNumber, expectedParentHash) + block, err := bs.bcm.getBlockByNumber(blockNumber, true, expectedParentHash) if err != nil { return nil, err } diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index 39d2dd8a..5555b240 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -33,14 +33,14 @@ import ( "github.com/stretchr/testify/mock" ) -func newTestBlockConfirmationManager(t *testing.T, enabled bool) (*blockConfirmationManager, *ffcapimocks.API) { +func newTestBlockConfirmationManager() (*blockConfirmationManager, *ffcapimocks.API) { tmconfig.Reset() config.Set(tmconfig.ConfirmationsRequired, 3) config.Set(tmconfig.ConfirmationsNotificationQueueLength, 1) - return newTestBlockConfirmationManagerCustomConfig(t) + return newTestBlockConfirmationManagerCustomConfig() } -func newTestBlockConfirmationManagerCustomConfig(t *testing.T) (*blockConfirmationManager, *ffcapimocks.API) { +func newTestBlockConfirmationManagerCustomConfig() (*blockConfirmationManager, *ffcapimocks.API) { logrus.SetLevel(logrus.DebugLevel) mca := &ffcapimocks.API{} emm := &metricsmocks.EventMetricsEmitter{} @@ -58,7 +58,7 @@ func newTestBlockConfirmationManagerCustomConfig(t *testing.T) (*blockConfirmati } func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, true) + bcm, mca := newTestBlockConfirmationManager() confirmed := make(chan *apitypes.ConfirmationsNotification, 1) eventToConfirm := &EventInfo{ @@ -180,7 +180,7 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { } func TestBlockConfirmationManagerE2EFork(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, true) + bcm, mca := newTestBlockConfirmationManager() confirmed := make(chan *apitypes.ConfirmationsNotification, 1) eventToConfirm := &EventInfo{ @@ -328,7 +328,7 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { } func TestBlockConfirmationManagerE2EForkReNotifyConfirmations(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, true) + bcm, mca := newTestBlockConfirmationManager() confirmed := make(chan *apitypes.ConfirmationsNotification, 3) eventToConfirm := &EventInfo{ @@ -461,7 +461,7 @@ func TestBlockConfirmationManagerE2EForkReNotifyConfirmations(t *testing.T) { } func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, true) + bcm, mca := newTestBlockConfirmationManager() bcm.fetchReceiptUponEntry = true // mark fetch receipt upon entry to do a fetch receipt before any blocks were retrieved confirmed := make(chan *apitypes.ConfirmationsNotification, 1) @@ -518,11 +518,13 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { }, } }).Return(&ffcapi.TransactionReceiptResponse{ - BlockHash: block1002a.ParentHash, - BlockNumber: fftypes.NewFFBigInt(1001), - TransactionIndex: fftypes.NewFFBigInt(0), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(1001).Int64(), fftypes.NewFFBigInt(0).Int64()), - Success: true, + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockHash: block1002a.ParentHash, + BlockNumber: fftypes.NewFFBigInt(1001), + TransactionIndex: fftypes.NewFFBigInt(0), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(1001).Int64(), fftypes.NewFFBigInt(0).Int64()), + Success: true, + }, }, ffcapi.ErrorReason(""), nil).Once() mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { @@ -557,11 +559,13 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { mca.On("TransactionReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.TransactionReceiptRequest) bool { return r.TransactionHash == txToConfirmForkA.TransactionHash })).Return(&ffcapi.TransactionReceiptResponse{ - BlockHash: block1001b.BlockHash, - BlockNumber: fftypes.NewFFBigInt(1001), - TransactionIndex: fftypes.NewFFBigInt(0), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(1001).Int64(), fftypes.NewFFBigInt(0).Int64()), - Success: true, + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockHash: block1001b.BlockHash, + BlockNumber: fftypes.NewFFBigInt(1001), + TransactionIndex: fftypes.NewFFBigInt(0), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(1001).Int64(), fftypes.NewFFBigInt(0).Int64()), + Success: true, + }, }, ffcapi.ErrorReason(""), nil).Once() // Then we get the final fork up to our confirmation @@ -648,7 +652,7 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { } func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, true) + bcm, mca := newTestBlockConfirmationManager() confirmed := make(chan []*apitypes.Confirmation, 1) eventToConfirm := &EventInfo{ @@ -751,7 +755,7 @@ func TestSortPendingEvents(t *testing.T) { func TestConfirmationsListenerFailWalkingChain(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() bcm.done = make(chan struct{}) err := bcm.Notify(&Notification{ @@ -780,7 +784,7 @@ func TestConfirmationsListenerFailWalkingChain(t *testing.T) { func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() bcm.done = make(chan struct{}) confirmed := make(chan []*apitypes.Confirmation, 1) @@ -818,7 +822,7 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { func TestConfirmationsListenerRemoved(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() bcm.done = make(chan struct{}) lid := fftypes.NewUUID() @@ -859,7 +863,7 @@ func TestConfirmationsListenerRemoved(t *testing.T) { func TestConfirmationsRemoveEvent(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() bcm.done = make(chan struct{}) eventInfo := &EventInfo{ @@ -897,7 +901,7 @@ func TestConfirmationsRemoveEvent(t *testing.T) { func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() bcm.done = make(chan struct{}) eventNotification := &Notification{ @@ -937,7 +941,7 @@ func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { func TestConfirmationsRemoveTransaction(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() bcm.done = make(chan struct{}) txInfo := &TransactionInfo{ @@ -985,7 +989,7 @@ func TestConfirmationsRemoveTransaction(t *testing.T) { func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() pending := (&Notification{ Event: &EventInfo{ @@ -1019,7 +1023,7 @@ func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { func TestWalkChainForEventBlockLookupFail(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() pending := (&Notification{ Event: &EventInfo{ @@ -1047,7 +1051,7 @@ func TestWalkChainForEventBlockLookupFail(t *testing.T) { func TestProcessBlockHashesLookupFail(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() blockHash := "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8" mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { @@ -1063,7 +1067,7 @@ func TestProcessBlockHashesLookupFail(t *testing.T) { func TestProcessNotificationsSwallowsUnknownType(t *testing.T) { - bcm, _ := newTestBlockConfirmationManager(t, false) + bcm, _ := newTestBlockConfirmationManager() blocks := bcm.newBlockState() bcm.processNotifications([]*Notification{ {NotificationType: NotificationType("unknown")}, @@ -1072,7 +1076,7 @@ func TestProcessNotificationsSwallowsUnknownType(t *testing.T) { func TestGetBlockNotFound(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df" @@ -1097,7 +1101,7 @@ func TestPanicBadKey(t *testing.T) { func TestNotificationValidation(t *testing.T) { - bcm, _ := newTestBlockConfirmationManager(t, false) + bcm, _ := newTestBlockConfirmationManager() bcm.bcmNotifications = make(chan *Notification) err := bcm.Notify(&Notification{ @@ -1134,15 +1138,17 @@ func TestNotificationValidation(t *testing.T) { func TestCheckReceiptImmediateConfirm(t *testing.T) { - bcm, _ := newTestBlockConfirmationManager(t, false) + bcm, _ := newTestBlockConfirmationManager() bcm.requiredConfirmations = 0 receipt := &ffcapi.TransactionReceiptResponse{ - BlockHash: fftypes.NewRandB32().String(), - BlockNumber: fftypes.NewFFBigInt(1001), - TransactionIndex: fftypes.NewFFBigInt(0), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(1001).Int64(), fftypes.NewFFBigInt(0).Int64()), - Success: true, + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockHash: fftypes.NewRandB32().String(), + BlockNumber: fftypes.NewFFBigInt(1001), + TransactionIndex: fftypes.NewFFBigInt(0), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(1001).Int64(), fftypes.NewFFBigInt(0).Int64()), + Success: true, + }, } done := make(chan struct{}) @@ -1162,13 +1168,15 @@ func TestCheckReceiptImmediateConfirm(t *testing.T) { func TestCheckReceiptWalkFail(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() receipt := &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", - TransactionIndex: fftypes.NewFFBigInt(10), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", + TransactionIndex: fftypes.NewFFBigInt(10), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + }, } mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 12346 @@ -1188,7 +1196,7 @@ func TestCheckReceiptWalkFail(t *testing.T) { func TestScheduleReceiptCheck(t *testing.T) { - bcm, _ := newTestBlockConfirmationManager(t, false) + bcm, _ := newTestBlockConfirmationManager() emm := &metricsmocks.EventMetricsEmitter{} emm.On("RecordNotificationQueueingMetrics", mock.Anything, mock.Anything, mock.Anything).Maybe() emm.On("RecordBlockHashProcessMetrics", mock.Anything, mock.Anything).Maybe() @@ -1221,7 +1229,7 @@ func TestScheduleReceiptCheck(t *testing.T) { func TestBlockState(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() block1002 := &ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ diff --git a/internal/confirmations/confirmed_block_listener.go b/internal/confirmations/confirmed_block_listener.go new file mode 100644 index 00000000..a03b245a --- /dev/null +++ b/internal/confirmations/confirmed_block_listener.go @@ -0,0 +1,379 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 confirmations + +import ( + "context" + "fmt" + "strconv" + "sync" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-common/pkg/retry" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +// confirmedBlockListener works differently to the main confirmation listener function, +// as an individual checkpoint-restart ordered stream of all blocks from the chain +// that have met a configured threshold of confirmations. +// +// Note that this builds upon the connector-specific block listener, which likely itself +// have some detailed handling of re-orgs at the front of the chian (the EVM one does). +// +// This implementation is thus deliberately simple assuming that when instability is found +// in the notifications it can simply wipe out its view and start again. +type confirmedBlockListener struct { + bcm *blockConfirmationManager + ctx context.Context + cancelFunc func() + id *fftypes.UUID + stateLock sync.Mutex + fromBlock uint64 + waitingForFromBlock bool + rollingCheckpoint *ffcapi.BlockListenerCheckpoint + blocksSinceCheckpoint []*apitypes.BlockInfo + newHeadToAdd []*apitypes.BlockInfo // used by the notification routine when there are new blocks that add directly onto the end of the blocksSinceCheckpoint + newBlockHashes chan *ffcapi.BlockHashEvent + dispatcherTap chan struct{} + eventStream chan<- *ffcapi.ListenerEvent + connector ffcapi.API + requiredConfirmations int + retry *retry.Retry + processorDone chan struct{} + dispatcherDone chan struct{} +} + +func (bcm *blockConfirmationManager) StartConfirmedBlockListener(ctx context.Context, id *fftypes.UUID, fromBlock string, checkpoint *ffcapi.BlockListenerCheckpoint, eventStream chan<- *ffcapi.ListenerEvent) error { + _, err := bcm.startConfirmedBlockListener(ctx, id, fromBlock, checkpoint, eventStream) + return err +} + +func (bcm *blockConfirmationManager) startConfirmedBlockListener(fgCtx context.Context, id *fftypes.UUID, fromBlock string, checkpoint *ffcapi.BlockListenerCheckpoint, eventStream chan<- *ffcapi.ListenerEvent) (cbl *confirmedBlockListener, err error) { + cbl = &confirmedBlockListener{ + bcm: bcm, + // We need our own listener for each confirmed block stream, and the bcm has to fan out + newBlockHashes: make(chan *ffcapi.BlockHashEvent, config.GetInt(tmconfig.ConfirmationsBlockQueueLength)), + dispatcherTap: make(chan struct{}, 1), + id: id, + eventStream: eventStream, + requiredConfirmations: bcm.requiredConfirmations, + connector: bcm.connector, + retry: bcm.retry, + rollingCheckpoint: checkpoint, + processorDone: make(chan struct{}), + dispatcherDone: make(chan struct{}), + } + cbl.ctx, cbl.cancelFunc = context.WithCancel(bcm.ctx) + // add a log context for this specific confirmation manager (as there are many within the ) + cbl.ctx = log.WithLogField(cbl.ctx, "role", fmt.Sprintf("confirmed_block_stream_%s", id)) + + switch fromBlock { + case "", ffcapi.FromBlockLatest: + if checkpoint != nil { + cbl.fromBlock = checkpoint.Block + } else { + cbl.waitingForFromBlock = true + } + case ffcapi.FromBlockEarliest: + fromBlock = "0" + fallthrough + default: + if cbl.fromBlock, err = strconv.ParseUint(fromBlock, 10, 64); err != nil { + return nil, i18n.NewError(fgCtx, tmmsgs.MsgFromBlockInvalid, fromBlock) + } + } + + bcm.cblLock.Lock() + defer bcm.cblLock.Unlock() + if _, alreadyStarted := bcm.cbls[*id]; alreadyStarted { + return nil, i18n.NewError(fgCtx, tmmsgs.MsgBlockListenerAlreadyStarted, id) + } + bcm.cbls[*id] = cbl + + go cbl.notificationProcessor() + go cbl.dispatcher() + return cbl, nil +} + +func (bcm *blockConfirmationManager) StopConfirmedBlockListener(fgCtx context.Context, id *fftypes.UUID) error { + bcm.cblLock.Lock() + defer bcm.cblLock.Unlock() + + cbl := bcm.cbls[*id] + if cbl == nil { + return i18n.NewError(fgCtx, tmmsgs.MsgBlockListenerNotStarted, id) + } + + // Don't hold lock while waiting, but do re-lock before deleting from the map + // (means multiple callers could enter this block in the middle, but that's re-entrant) + bcm.cblLock.Unlock() + cbl.cancelFunc() + <-cbl.processorDone + <-cbl.dispatcherDone + bcm.cblLock.Lock() + + delete(bcm.cbls, *id) + return nil +} + +// The notificationProcessor processes all notification immediately from the head of the chain +// regardless of how far back in the chain the dispatcher is. +func (cbl *confirmedBlockListener) notificationProcessor() { + defer close(cbl.processorDone) + for { + select { + case bhe := <-cbl.newBlockHashes: + cbl.processBlockHashes(bhe.BlockHashes) + case <-cbl.ctx.Done(): + log.L(cbl.ctx).Debugf("Confirmed block listener stopping") + return + } + } +} + +func (cbl *confirmedBlockListener) processBlockHashes(blockHashes []string) { + for _, blockHash := range blockHashes { + block, err := cbl.bcm.getBlockByHash(blockHash) + if err != nil || block == nil { + // regardless of the failure, as long as we get notified of subsequent + // blocks that work this will work itself out. + log.L(cbl.ctx).Errorf("Failed to retrieve block %s: %v", blockHash, err) + continue + } + cbl.processBlockNotification(block) + } +} + +// Whenever we get a new block we try and reconcile it into our current view of the +// canonical chain ahead of the last checkpoint. +// Then we update the state the dispatcher uses to walk forwards from and see what +// is confirmed and ready to dispatch +func (cbl *confirmedBlockListener) processBlockNotification(block *apitypes.BlockInfo) { + + cbl.stateLock.Lock() + defer cbl.stateLock.Unlock() + + if cbl.waitingForFromBlock { + // by definition we won't find anything in cbl.blocksSinceCheckpoint below + cbl.fromBlock = block.BlockNumber.Uint64() + cbl.waitingForFromBlock = false + } else if block.BlockNumber.Uint64() < cbl.fromBlock { + log.L(cbl.ctx).Debugf("Notification of block %d/%s < fromBlock %d", block.BlockNumber, block.BlockHash, cbl.fromBlock) + return + } + + // If the block is before our checkpoint, we ignore it completely + if cbl.rollingCheckpoint != nil && block.BlockNumber.Uint64() <= cbl.rollingCheckpoint.Block { + log.L(cbl.ctx).Debugf("Notification of block %d/%s <= checkpoint %d", block.BlockNumber, block.BlockHash, cbl.rollingCheckpoint.Block) + return + } + + // If the block immediate adds onto the set of blocks being processed, then we just attach it there + // and notify the dispatcher to process it directly. No need for the other routine to query again. + // When we're in steady state listening to the stable head of the chain, this should be the most common case. + var dispatchHead *apitypes.BlockInfo + if len(cbl.newHeadToAdd) > 0 { + // we've snuck in multiple notifications while the dispatcher is busy... don't add indefinitely to this list + if len(cbl.newHeadToAdd) > 10 /* not considered worth adding/explaining a tuning property for this */ { + log.L(cbl.ctx).Infof("Block listener fell behind head of chain") + cbl.newHeadToAdd = nil + } else { + dispatchHead = cbl.newHeadToAdd[len(cbl.newHeadToAdd)-1] + } + } + if dispatchHead == nil && len(cbl.blocksSinceCheckpoint) > 0 { + dispatchHead = cbl.blocksSinceCheckpoint[len(cbl.blocksSinceCheckpoint)-1] + } + switch { + case dispatchHead != nil && block.BlockNumber == dispatchHead.BlockNumber+1 && block.ParentHash == dispatchHead.BlockHash: + // Ok - we just need to pop it onto the list, and ensure we wake the dispatcher routine + log.L(cbl.ctx).Debugf("Directly passing block %d/%s to dispatcher after block %d/%s", block.BlockNumber, block.BlockHash, dispatchHead.BlockNumber, dispatchHead.BlockHash) + cbl.newHeadToAdd = append(cbl.newHeadToAdd, block) + case dispatchHead == nil && (cbl.rollingCheckpoint == nil || block.BlockNumber.Uint64() == (cbl.rollingCheckpoint.Block+1)): + // This is the next block the dispatcher needs, to wake it up with this. + log.L(cbl.ctx).Debugf("Directly passing block %d/%s to dispatcher as no blocks pending", block.BlockNumber, block.BlockHash) + cbl.newHeadToAdd = append(cbl.newHeadToAdd, block) + default: + // Otherwise see if it's a conflicting fork to any of our existing blocks + for idx, existingBlock := range cbl.blocksSinceCheckpoint { + if existingBlock.BlockNumber == block.BlockNumber { + // Must discard up to this point + cbl.blocksSinceCheckpoint = cbl.blocksSinceCheckpoint[0:idx] + cbl.newHeadToAdd = nil + // This block fits, slot it into this point in the chain + if idx == 0 || block.ParentHash == cbl.blocksSinceCheckpoint[idx-1].BlockHash { + log.L(cbl.ctx).Debugf("Notification of re-org %d/%s replacing block %d/%s", block.BlockNumber, block.BlockHash, existingBlock.BlockNumber, existingBlock.BlockHash) + cbl.blocksSinceCheckpoint = append(cbl.blocksSinceCheckpoint[0:idx], block) + } else { + log.L(cbl.ctx).Debugf("Notification of block %d/%s conflicting with previous block %d/%s", block.BlockNumber, block.BlockHash, existingBlock.BlockNumber, existingBlock.BlockHash) + } + break + } + } + } + + // There's something for the dispatcher to process + cbl.tapDispatcher() + +} + +func (cbl *confirmedBlockListener) tapDispatcher() { + select { + case cbl.dispatcherTap <- struct{}{}: + default: + } +} + +func (cbl *confirmedBlockListener) dispatcher() { + defer close(cbl.dispatcherDone) + + for { + if !cbl.waitingForFromBlock { + // spin getting blocks until we it looks like we need to wait for a notification + lastFromNotification := false + for cbl.readNextBlock(&lastFromNotification) { + cbl.dispatchAllConfirmed() + } + } + + select { + case <-cbl.dispatcherTap: + case <-cbl.ctx.Done(): + log.L(cbl.ctx).Debugf("Confirmed block dispatcher stopping") + return + } + + } +} + +// MUST be called under lock +func (cbl *confirmedBlockListener) popDispatchedIfAvailable(lastFromNotification *bool) (blockNumberToFetch uint64, found bool) { + + if len(cbl.newHeadToAdd) > 0 { + // If we find one in the lock, it must be ready for us to append + nextBlock := cbl.newHeadToAdd[0] + cbl.newHeadToAdd = append([]*apitypes.BlockInfo{}, cbl.newHeadToAdd[1:]...) + cbl.blocksSinceCheckpoint = append(cbl.blocksSinceCheckpoint, nextBlock) + + // We track that we've done this, so we know if we run out going round the loop later, + // there's no point in doing a get-by-number + *lastFromNotification = true + return 0, true + } + + blockNumberToFetch = cbl.fromBlock + if cbl.rollingCheckpoint != nil && cbl.rollingCheckpoint.Block >= cbl.fromBlock { + blockNumberToFetch = cbl.rollingCheckpoint.Block + 1 + } + if len(cbl.blocksSinceCheckpoint) > 0 { + blockNumberToFetch = cbl.blocksSinceCheckpoint[len(cbl.blocksSinceCheckpoint)-1].BlockNumber.Uint64() + 1 + } + return blockNumberToFetch, false +} + +func (cbl *confirmedBlockListener) readNextBlock(lastFromNotification *bool) (found bool) { + + var nextBlock *apitypes.BlockInfo + var blockNumberToFetch uint64 + var dispatchedPopped bool + err := cbl.retry.Do(cbl.ctx, "next block", func(_ int) (retry bool, err error) { + // If the notifier has lined up a block for us grab it before + cbl.stateLock.Lock() + blockNumberToFetch, dispatchedPopped = cbl.popDispatchedIfAvailable(lastFromNotification) + cbl.stateLock.Unlock() + if dispatchedPopped || *lastFromNotification { + // We processed a dispatch this time, or last time. + // Either way we're tracking at the head and there's no point doing a query + // we expect to return nothing - as we should get another notification. + return false, nil + } + + // Get the next block + nextBlock, err = cbl.bcm.getBlockByNumber(blockNumberToFetch, false, "") + return true, err + }) + if nextBlock == nil || err != nil { + // We either got a block dispatched, or did not find a block ourselves. + return dispatchedPopped + } + + // In the lock append it to our list, checking it's valid to append to what we have + cbl.stateLock.Lock() + defer cbl.stateLock.Unlock() + + // We have to check because we unlocked, that we weren't beaten to the punch while we queried + // by the dispatcher. + if _, dispatchedPopped = cbl.popDispatchedIfAvailable(lastFromNotification); !dispatchedPopped { + + // It's possible that while we were off at the node querying this, a notification came in + // that affected our state. We need to check this still matches, or go round again + if len(cbl.blocksSinceCheckpoint) > 0 { + if cbl.blocksSinceCheckpoint[len(cbl.blocksSinceCheckpoint)-1].BlockHash != nextBlock.ParentHash { + // This doesn't attach to the end of our list. Trim it off and try again. + cbl.blocksSinceCheckpoint = cbl.blocksSinceCheckpoint[0 : len(cbl.blocksSinceCheckpoint)-1] + return true + } + } + + // We successfully attached it + cbl.blocksSinceCheckpoint = append(cbl.blocksSinceCheckpoint, nextBlock) + } + return true + +} + +func (cbl *confirmedBlockListener) dispatchAllConfirmed() { + for { + var toDispatch *ffcapi.ListenerEvent + cbl.stateLock.Lock() + if len(cbl.blocksSinceCheckpoint) > cbl.requiredConfirmations { + block := cbl.blocksSinceCheckpoint[0] + // don't want memory to grow indefinitely by shifting right, so we create a new slice here + cbl.blocksSinceCheckpoint = append([]*apitypes.BlockInfo{}, cbl.blocksSinceCheckpoint[1:]...) + cbl.rollingCheckpoint = &ffcapi.BlockListenerCheckpoint{ + Block: block.BlockNumber.Uint64(), + } + toDispatch = &ffcapi.ListenerEvent{ + BlockEvent: &ffcapi.BlockEvent{ + ListenerID: cbl.id, + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block.BlockNumber)), + BlockHash: block.BlockHash, + ParentHash: block.ParentHash, + TransactionHashes: block.TransactionHashes, + }, + }, + Checkpoint: cbl.rollingCheckpoint, + } + } + cbl.stateLock.Unlock() + if toDispatch == nil { + return + } + log.L(cbl.ctx).Infof("Dispatching block %d/%s", toDispatch.BlockEvent.BlockNumber.Uint64(), toDispatch.BlockEvent.BlockHash) + select { + case cbl.eventStream <- toDispatch: + case <-cbl.ctx.Done(): + } + } +} diff --git a/internal/confirmations/confirmed_block_listener_test.go b/internal/confirmations/confirmed_block_listener_test.go new file mode 100644 index 00000000..3c3f39c3 --- /dev/null +++ b/internal/confirmations/confirmed_block_listener_test.go @@ -0,0 +1,522 @@ +// Copyright 2019 Kaleido + +// 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 confirmations + +import ( + "fmt" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestCBLCatchUpToHeadFromZeroNoConfirmations(t *testing.T) { + bcm, mca := newTestBlockConfirmationManager() + + esDispatch := make(chan *ffcapi.ListenerEvent) + + id := fftypes.NewUUID() + + blocks := testBlockArray(10) + + mbiNum := mca.On("BlockInfoByNumber", mock.Anything, mock.Anything) + mbiNum.Run(func(args mock.Arguments) { mockBlockNumberReturn(mbiNum, args, blocks) }) + + bcm.requiredConfirmations = 0 + cbl, err := bcm.startConfirmedBlockListener(bcm.ctx, id, ffcapi.FromBlockEarliest, nil, esDispatch) + assert.NoError(t, err) + + for i := 0; i < len(blocks)-bcm.requiredConfirmations; i++ { + b := <-esDispatch + assert.Equal(t, b.BlockEvent.BlockInfo, blocks[i].BlockInfo) + } + + time.Sleep(1 * time.Millisecond) + assert.Empty(t, cbl.blocksSinceCheckpoint) + + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestCBLCatchUpToHeadFromZeroWithConfirmations(t *testing.T) { + bcm, mca := newTestBlockConfirmationManager() + + esDispatch := make(chan *ffcapi.ListenerEvent) + + id := fftypes.NewUUID() + + blocks := testBlockArray(15) + + mbiNum := mca.On("BlockInfoByNumber", mock.Anything, mock.Anything) + mbiNum.Run(func(args mock.Arguments) { mockBlockNumberReturn(mbiNum, args, blocks) }) + + bcm.requiredConfirmations = 5 + cbl, err := bcm.startConfirmedBlockListener(bcm.ctx, id, ffcapi.FromBlockEarliest, nil, esDispatch) + assert.NoError(t, err) + + for i := 0; i < len(blocks)-bcm.requiredConfirmations; i++ { + b := <-esDispatch + assert.Equal(t, b.BlockEvent.BlockInfo, blocks[i].BlockInfo) + } + + time.Sleep(1 * time.Millisecond) + assert.Len(t, cbl.blocksSinceCheckpoint, bcm.requiredConfirmations) + select { + case <-esDispatch: + assert.Fail(t, "should not have received block in confirmation window") + default: // good - we should have the confirmations sat there, but no dispatch + } + + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestCBLListenFromCurrentBlock(t *testing.T) { + bcm, mca := newTestBlockConfirmationManager() + + esDispatch := make(chan *ffcapi.ListenerEvent) + + id := fftypes.NewUUID() + + blocks := testBlockArray(15) + + mbiHash := mca.On("BlockInfoByHash", mock.Anything, mock.Anything) + mbiHash.Run(func(args mock.Arguments) { mockBlockHashReturn(mbiHash, args, blocks) }) + + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Maybe() + + bcm.requiredConfirmations = 5 + cbl, err := bcm.startConfirmedBlockListener(bcm.ctx, id, "", nil, esDispatch) + assert.NoError(t, err) + + // Notify starting at block 5 + for i := 5; i < len(blocks); i++ { + bcm.propagateBlockHashToCBLs(&ffcapi.BlockHashEvent{ + BlockHashes: []string{blocks[i].BlockHash}, + }) + } + + // Randomly notify below that too, which will be ignored + bcm.propagateBlockHashToCBLs(&ffcapi.BlockHashEvent{ + BlockHashes: []string{blocks[1].BlockHash}, + }) + + for i := 5; i < len(blocks)-bcm.requiredConfirmations; i++ { + b := <-esDispatch + assert.Equal(t, b.BlockEvent.BlockNumber, blocks[i].BlockNumber) + assert.Equal(t, b.BlockEvent.BlockInfo, blocks[i].BlockInfo) + } + + time.Sleep(1 * time.Millisecond) + assert.Len(t, cbl.blocksSinceCheckpoint, bcm.requiredConfirmations) + select { + case <-esDispatch: + assert.Fail(t, "should not have received block in confirmation window") + default: // good - we should have the confirmations sat there, but no dispatch + } + + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestCBLListenFromCurrentUsingCheckpointBlock(t *testing.T) { + bcm, mca := newTestBlockConfirmationManager() + + esDispatch := make(chan *ffcapi.ListenerEvent) + + id := fftypes.NewUUID() + + blocks := testBlockArray(0) + + mbiNum := mca.On("BlockInfoByNumber", mock.Anything, mock.Anything) + mbiNum.Run(func(args mock.Arguments) { mockBlockNumberReturn(mbiNum, args, blocks) }) + + bcm.requiredConfirmations = 5 + cbl, err := bcm.startConfirmedBlockListener(bcm.ctx, id, "", &ffcapi.BlockListenerCheckpoint{ + Block: 12345, + }, esDispatch) + assert.NoError(t, err) + + assert.False(t, cbl.waitingForFromBlock) + assert.Equal(t, uint64(12345), cbl.fromBlock) + + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestCBLHandleReorgInConfirmationWindow1(t *testing.T) { + // test where the reorg happens at the edge of the confirmation window + testCBLHandleReorgInConfirmationWindow(t, + 10, // blocks in chain before re-org + 5, // blocks that remain from original chain after re-org + 5, // required confirmations + ) +} + +func TestCBLHandleReorgInConfirmationWindow2(t *testing.T) { + // test where the reorg happens replacing some blocks + // WE ALREADY CONFIRMED - meaning we dispatched them incorrectly + // because the confirmations were not tuned correctly + testCBLHandleReorgInConfirmationWindow(t, + 10, // blocks in chain before re-org + 0, // blocks that remain from original chain after re-org + 5, // required confirmations + ) +} + +func TestCBLHandleReorgInConfirmationWindow3(t *testing.T) { + // test without confirmations, so everything is a problem + testCBLHandleReorgInConfirmationWindow(t, + 10, // blocks in chain before re-org + 0, // blocks that remain from original chain after re-org + 0, // required confirmations + ) +} + +func TestCBLHandleReorgInConfirmationWindow4(t *testing.T) { + // test of a re-org of one + testCBLHandleReorgInConfirmationWindow(t, + 5, // blocks in chain before re-org + 4, // blocks that remain from original chain after re-org + 4, // required confirmations + ) +} + +func testCBLHandleReorgInConfirmationWindow(t *testing.T, blockLenBeforeReorg, overlap, reqConf int) { + bcm, mca := newTestBlockConfirmationManager() + + esDispatch := make(chan *ffcapi.ListenerEvent) + + id := fftypes.NewUUID() + blocksBeforeReorg := testBlockArray(blockLenBeforeReorg) + blocksAfterReorg := testBlockArray(blockLenBeforeReorg + overlap) + if overlap > 0 { + copy(blocksAfterReorg, blocksBeforeReorg[0:overlap]) + blocksAfterReorg[overlap] = &ffcapi.BlockInfoByNumberResponse{ + BlockInfo: blocksAfterReorg[overlap].BlockInfo, + } + blocksAfterReorg[overlap].ParentHash = blocksAfterReorg[overlap-1].BlockHash + } + checkBlocksSequential(t, "before", blocksBeforeReorg) + checkBlocksSequential(t, "after ", blocksAfterReorg) + + var cbl *confirmedBlockListener + var isAfterReorg atomic.Bool + notificationsDone := make(chan struct{}) + mbiNum := mca.On("BlockInfoByNumber", mock.Anything, mock.Anything) + mbiNum.Run(func(args mock.Arguments) { + if isAfterReorg.Load() { + mockBlockNumberReturn(mbiNum, args, blocksAfterReorg) + } else { + mockBlockNumberReturn(mbiNum, args, blocksBeforeReorg) + // we instigate the re-org when we've returned all the blocks + if int(args[1].(*ffcapi.BlockInfoByNumberRequest).BlockNumber.Int64()) >= len(blocksBeforeReorg) { + isAfterReorg.Store(true) + go func() { + defer close(notificationsDone) + // Simulate the modified blocks only coming in with delays + for i := overlap; i < len(blocksAfterReorg); i++ { + time.Sleep(100 * time.Microsecond) + bcm.propagateBlockHashToCBLs(&ffcapi.BlockHashEvent{ + BlockHashes: []string{blocksAfterReorg[i].BlockHash}, + }) + } + }() + } + } + }) + // We query by hash only for the notifications, which are only on the after-reorg blocks + mbiHash := mca.On("BlockInfoByHash", mock.Anything, mock.Anything) + mbiHash.Run(func(args mock.Arguments) { mockBlockHashReturn(mbiHash, args, blocksAfterReorg) }) + + bcm.requiredConfirmations = reqConf + cbl, err := bcm.startConfirmedBlockListener(bcm.ctx, id, ffcapi.FromBlockEarliest, nil, esDispatch) + assert.NoError(t, err) + + for i := 0; i < len(blocksAfterReorg)-bcm.requiredConfirmations; i++ { + b := <-esDispatch + dangerArea := len(blocksAfterReorg) - overlap + if i >= overlap && i < (dangerArea-reqConf) { + // This would be a bad situation in reality, where a reorg crossed the confirmations + // boundary. An indication someone incorrectly configured their confirmations + assert.Equal(t, b.BlockEvent.BlockInfo, blocksBeforeReorg[i].BlockInfo) + } else { + assert.Equal(t, b.BlockEvent.BlockInfo, blocksAfterReorg[i].BlockInfo) + } + } + + time.Sleep(1 * time.Millisecond) + assert.LessOrEqual(t, len(cbl.blocksSinceCheckpoint), bcm.requiredConfirmations) + select { + case b := <-esDispatch: + assert.Fail(t, fmt.Sprintf("should not have received block in confirmation window: %d/%s", b.BlockEvent.BlockNumber.Int64(), b.BlockEvent.BlockHash)) + default: // good - we should have the confirmations sat there, but no dispatch + } + + // Wait for the notifications to go through + <-notificationsDone + + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestCBLHandleRandomConflictingBlockNotification(t *testing.T) { + bcm, mca := newTestBlockConfirmationManager() + + esDispatch := make(chan *ffcapi.ListenerEvent) + + id := fftypes.NewUUID() + blocks := testBlockArray(50) + + randBlock := &ffcapi.BlockInfoByHashResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(3), + BlockHash: fftypes.NewRandB32().String(), + ParentHash: fftypes.NewRandB32().String(), + }, + } + + var cbl *confirmedBlockListener + sentRandom := false + mbiNum := mca.On("BlockInfoByNumber", mock.Anything, mock.Anything) + mbiNum.Run(func(args mock.Arguments) { + mockBlockNumberReturn(mbiNum, args, blocks) + if !sentRandom && args[1].(*ffcapi.BlockInfoByNumberRequest).BlockNumber.Int64() == 4 { + sentRandom = true + bcm.propagateBlockHashToCBLs(&ffcapi.BlockHashEvent{ + BlockHashes: []string{randBlock.BlockHash}, + }) + // Give notification handler likelihood to run before we continue the by-number getting + time.Sleep(1 * time.Millisecond) + } + }) + // We query by hash only for the notifications, which are only on the after-reorg blocks + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(req *ffcapi.BlockInfoByHashRequest) bool { + return req.BlockHash == randBlock.BlockHash + })).Return(randBlock, ffcapi.ErrorReason(""), nil) + + cbl, err := bcm.startConfirmedBlockListener(bcm.ctx, id, ffcapi.FromBlockEarliest, nil, esDispatch) + assert.NoError(t, err) + cbl.requiredConfirmations = 5 + + for i := 0; i < len(blocks)-cbl.requiredConfirmations; i++ { + b := <-esDispatch + assert.Equal(t, b.BlockEvent.BlockInfo, blocks[i].BlockInfo) + } + + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestCBLDispatcherFallsBehindHead(t *testing.T) { + bcm, mca := newTestBlockConfirmationManager() + + esDispatch := make(chan *ffcapi.ListenerEvent) + + id := fftypes.NewUUID() + + blocks := testBlockArray(30) + + mbiHash := mca.On("BlockInfoByHash", mock.Anything, mock.Anything) + mbiHash.Run(func(args mock.Arguments) { mockBlockHashReturn(mbiHash, args, blocks) }) + + // We'll fall back to this because we don't keep up + mbiNum := mca.On("BlockInfoByNumber", mock.Anything, mock.Anything) + mbiNum.Run(func(args mock.Arguments) { mockBlockNumberReturn(mbiNum, args, blocks) }) + + bcm.requiredConfirmations = 5 + cbl, err := bcm.startConfirmedBlockListener(bcm.ctx, id, "", nil, esDispatch) + assert.NoError(t, err) + + // Notify all the blocks before we process any + for i := 0; i < len(blocks); i++ { + bcm.propagateBlockHashToCBLs(&ffcapi.BlockHashEvent{ + BlockHashes: []string{blocks[i].BlockHash}, + }) + } + + for i := 0; i < len(blocks)-bcm.requiredConfirmations; i++ { + // The dispatches should have been added, until it got too far ahead + // and then set to nil. + for cbl.newHeadToAdd != nil { + time.Sleep(1 * time.Millisecond) + } + b := <-esDispatch + assert.Equal(t, b.BlockEvent.BlockNumber.Uint64(), blocks[i].BlockNumber.Uint64()) + assert.Equal(t, b.BlockEvent.BlockInfo, blocks[i].BlockInfo) + } + + time.Sleep(1 * time.Millisecond) + assert.Len(t, cbl.blocksSinceCheckpoint, bcm.requiredConfirmations) + select { + case <-esDispatch: + assert.Fail(t, "should not have received block in confirmation window") + default: // good - we should have the confirmations sat there, but no dispatch + } + + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestCBLStartBadFromBlock(t *testing.T) { + bcm, mca := newTestBlockConfirmationManager() + + esDispatch := make(chan *ffcapi.ListenerEvent) + + id := fftypes.NewUUID() + + _, err := bcm.startConfirmedBlockListener(bcm.ctx, id, "wrong", nil, esDispatch) + assert.Regexp(t, "FF21090", err) + + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestProcessBlockHashesSwallowsFailure(t *testing.T) { + bcm, mca := newTestBlockConfirmationManager() + cbl := &confirmedBlockListener{ + ctx: bcm.ctx, + bcm: bcm, + } + blocks := testBlockArray(1) + mbiHash := mca.On("BlockInfoByHash", mock.Anything, mock.Anything) + mbiHash.Return(nil, ffcapi.ErrorReasonDownstreamDown, fmt.Errorf("nope")) + cbl.processBlockHashes([]string{blocks[0].BlockHash}) + bcm.Stop() + mca.AssertExpectations(t) +} + +func TestDispatchAllConfirmedNonBlocking(t *testing.T) { + bcm, _ := newTestBlockConfirmationManager() + cbl := &confirmedBlockListener{ + id: fftypes.NewUUID(), + ctx: bcm.ctx, + bcm: bcm, + processorDone: make(chan struct{}), + eventStream: make(chan<- *ffcapi.ListenerEvent), // blocks indefinitely + } + + cbl.requiredConfirmations = 0 + cbl.blocksSinceCheckpoint = []*apitypes.BlockInfo{ + {BlockNumber: fftypes.FFuint64(12345), BlockHash: fftypes.NewRandB32().String()}, + } + waitForDispatchReturn := make(chan struct{}) + go func() { + defer close(waitForDispatchReturn) + cbl.dispatchAllConfirmed() + }() + + bcm.cancelFunc() + bcm.Stop() + <-waitForDispatchReturn + + // Ensure if there were a pending dispatch it wouldn't block + close(cbl.processorDone) + bcm.cbls = map[fftypes.UUID]*confirmedBlockListener{*cbl.id: cbl} + bcm.propagateBlockHashToCBLs(&ffcapi.BlockHashEvent{}) +} + +func TestCBLDoubleStart(t *testing.T) { + id := fftypes.NewUUID() + + bcm, mca := newTestBlockConfirmationManager() + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) + + err := bcm.StartConfirmedBlockListener(bcm.ctx, id, ffcapi.FromBlockEarliest, nil, make(chan<- *ffcapi.ListenerEvent)) + assert.NoError(t, err) + + err = bcm.StartConfirmedBlockListener(bcm.ctx, id, ffcapi.FromBlockEarliest, nil, make(chan<- *ffcapi.ListenerEvent)) + assert.Regexp(t, "FF21087", err) + + bcm.Stop() +} + +func TestCBLStopNotStarted(t *testing.T) { + id := fftypes.NewUUID() + + bcm, _ := newTestBlockConfirmationManager() + + err := bcm.StopConfirmedBlockListener(bcm.ctx, id) + assert.Regexp(t, "FF21088", err) + + bcm.Stop() +} + +func testBlockArray(l int) []*ffcapi.BlockInfoByNumberResponse { + blocks := make([]*ffcapi.BlockInfoByNumberResponse, l) + for i := 0; i < l; i++ { + blocks[i] = &ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(i)), + BlockHash: fftypes.NewRandB32().String(), + }, + } + if i == 0 { + blocks[i].ParentHash = fftypes.NewRandB32().String() + } else { + blocks[i].ParentHash = blocks[i-1].BlockHash + } + } + return blocks +} + +func mockBlockNumberReturn(mbiBlockInfoByNumber *mock.Call, args mock.Arguments, blocks []*ffcapi.BlockInfoByNumberResponse) { + req := args[1].(*ffcapi.BlockInfoByNumberRequest) + blockNo := int(req.BlockNumber.Uint64()) + if blockNo >= len(blocks) { + mbiBlockInfoByNumber.Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) + } else { + mbiBlockInfoByNumber.Return(blocks[blockNo], ffcapi.ErrorReason(""), nil) + } +} + +func mockBlockHashReturn(mbiBlockInfoByHash *mock.Call, args mock.Arguments, blocks []*ffcapi.BlockInfoByNumberResponse) { + req := args[1].(*ffcapi.BlockInfoByHashRequest) + for _, b := range blocks { + if b.BlockHash == req.BlockHash { + mbiBlockInfoByHash.Return(&ffcapi.BlockInfoByHashResponse{ + BlockInfo: b.BlockInfo, + }, ffcapi.ErrorReason(""), nil) + return + } + } + mbiBlockInfoByHash.Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) +} + +func checkBlocksSequential(t *testing.T, desc string, blocks []*ffcapi.BlockInfoByNumberResponse) { + blockHashes := make([]string, len(blocks)) + var lastBlock *ffcapi.BlockInfoByNumberResponse + invalid := false + for i, b := range blocks { + assert.NotEmpty(t, b.BlockHash) + blockHashes[i] = b.BlockHash + if i == 0 { + assert.NotEmpty(t, b.ParentHash) + } else if lastBlock.BlockHash != b.ParentHash { + invalid = true + } + lastBlock = b + } + fmt.Printf("%s: %s\n", desc, strings.Join(blockHashes, ",")) + if invalid { + panic("wrong sequence") // aid to writing tests that build sequences + } +} diff --git a/internal/confirmations/receipt_checker.go b/internal/confirmations/receipt_checker.go index e4d2861d..f3b90101 100644 --- a/internal/confirmations/receipt_checker.go +++ b/internal/confirmations/receipt_checker.go @@ -94,7 +94,7 @@ func (rc *receiptChecker) run(i int) { // We use the back-off retry handling of the retry loop to avoid tight loops, // but in the case of errors we re-queue the individual item to the back of the // queue so individual queued items do not get stuck for unrecoverable errors. - err := rc.bcm.retry.Do(ctx, "receipt check", func(attempt int) (bool, error) { + err := rc.bcm.retry.Do(ctx, "receipt check", func(_ int) (bool, error) { startTime := time.Now() pending := rc.waitNext() if pending == nil { diff --git a/internal/confirmations/receipt_checker_test.go b/internal/confirmations/receipt_checker_test.go index a42fb96d..f71f6187 100644 --- a/internal/confirmations/receipt_checker_test.go +++ b/internal/confirmations/receipt_checker_test.go @@ -27,7 +27,7 @@ import ( func TestCheckReceiptNotFoundErr(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() mca.On("TransactionReceipt", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { @@ -53,7 +53,7 @@ func TestCheckReceiptNotFoundErr(t *testing.T) { func TestCheckReceiptNotFoundNil(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() mca.On("TransactionReceipt", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { @@ -79,7 +79,7 @@ func TestCheckReceiptNotFoundNil(t *testing.T) { func TestCheckReceiptFail(t *testing.T) { - bcm, mca := newTestBlockConfirmationManager(t, false) + bcm, mca := newTestBlockConfirmationManager() count := 0 mca.On("TransactionReceipt", mock.Anything, mock.Anything). @@ -110,7 +110,7 @@ func TestCheckReceiptFail(t *testing.T) { func TestCheckReceiptDoubleQueueProtection(t *testing.T) { - bcm, _ := newTestBlockConfirmationManager(t, false) + bcm, _ := newTestBlockConfirmationManager() txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" pending := &pendingItem{ diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index f980c8fe..9de8b39e 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -19,6 +19,7 @@ package events import ( "context" "encoding/json" + "fmt" "sync" "time" @@ -100,19 +101,20 @@ type startedStreamState struct { } type eventStream struct { - bgCtx context.Context - spec *apitypes.EventStream - mux sync.Mutex - status apitypes.EventStreamStatus - connector ffcapi.API - persistence persistence.Persistence - confirmations confirmations.Manager - listeners map[fftypes.UUID]*listener - wsChannels ws.WebSocketChannels - retry *retry.Retry - currentState *startedStreamState - checkpointInterval time.Duration - batchChannel chan *ffcapi.ListenerEvent + bgCtx context.Context + spec *apitypes.EventStream + mux sync.Mutex + status apitypes.EventStreamStatus + connector ffcapi.API + persistence persistence.Persistence + confirmations confirmations.Manager + confirmationsRequired int + listeners map[fftypes.UUID]*listener + wsChannels ws.WebSocketChannels + retry *retry.Retry + currentState *startedStreamState + checkpointInterval time.Duration + batchChannel chan *ffcapi.ListenerEvent } func NewEventStream( @@ -126,18 +128,17 @@ func NewEventStream( ) (ees Stream, err error) { esCtx := log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()) es := &eventStream{ - bgCtx: esCtx, - status: apitypes.EventStreamStatusStopped, - spec: persistedSpec, - connector: connector, - persistence: persistence, - listeners: make(map[fftypes.UUID]*listener), - wsChannels: wsChannels, - retry: esDefaults.retry, - checkpointInterval: config.GetDuration(tmconfig.EventStreamsCheckpointInterval), - } - if config.GetInt(tmconfig.ConfirmationsRequired) > 0 { - es.confirmations = confirmations.NewBlockConfirmationManager(esCtx, connector, "_es_"+persistedSpec.ID.String(), eme) + bgCtx: esCtx, + status: apitypes.EventStreamStatusStopped, + spec: persistedSpec, + connector: connector, + persistence: persistence, + listeners: make(map[fftypes.UUID]*listener), + wsChannels: wsChannels, + retry: esDefaults.retry, + checkpointInterval: config.GetDuration(tmconfig.EventStreamsCheckpointInterval), + confirmations: confirmations.NewBlockConfirmationManager(esCtx, connector, "_es_"+persistedSpec.ID.String(), eme), + confirmationsRequired: config.GetInt(tmconfig.ConfirmationsRequired), } // The configuration we have in memory, applies all the defaults to what is passed in // to ensure there are no nil fields on the configuration object. @@ -305,6 +306,10 @@ func (es *eventStream) mergeListenerOptions(id *fftypes.UUID, updates *apitypes. merged := *base + if updates.Type != nil && base.Type == nil { + merged.Type = updates.Type // cannot change, but must be settable on create + } + if updates.Name != nil { merged.Name = updates.Name } @@ -346,22 +351,37 @@ func (es *eventStream) verifyListenerOptions(ctx context.Context, id *fftypes.UU // Merge the supplied options with defaults and any existing config. spec := es.mergeListenerOptions(id, updatesOrNew) - // The connector needs to validate the options, building a set of options that are assured to be non-nil - res, _, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.EventListenerVerifyOptionsRequest{ - EventListenerOptions: listenerSpecToOptions(spec), - }) - if err != nil { - return nil, i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) + if spec.Type == nil { + spec.Type = &apitypes.ListenerTypeEvents } + switch *spec.Type { + case apitypes.ListenerTypeEvents: + // The connector needs to validate the options, building a set of options that are assured to be non-nil + res, _, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.EventListenerVerifyOptionsRequest{ + EventListenerOptions: listenerSpecToOptions(spec), + }) + if err != nil { + return nil, i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) + } - // We update the spec object in-place for the signature and resolved options - spec.Signature = &res.ResolvedSignature - spec.Options = &res.ResolvedOptions - if spec.Name == nil || *spec.Name == "" { - sig := spec.Signature - spec.Name = sig + // We update the spec object in-place for the signature and resolved options + spec.Signature = &res.ResolvedSignature + spec.Options = &res.ResolvedOptions + if spec.Name == nil || *spec.Name == "" { + sig := spec.Signature + spec.Name = sig + } + log.L(ctx).Infof("Listener %s signature: %s", spec.ID, *spec.Signature) + case apitypes.ListenerTypeBlocks: + // other fields not applicable currently for block listeners + spec.Signature = ptrTo("") + spec.Filters = apitypes.ListenerFilters{} + spec.Options = fftypes.JSONAnyPtr(`{}`) + log.L(ctx).Infof("BLock listener %s", spec.ID) + default: + return nil, i18n.NewError(ctx, tmmsgs.MsgBadListenerType, *spec.Type) } - log.L(ctx).Infof("Listener %s signature: %s", spec.ID, *spec.Signature) + return spec, nil } @@ -397,6 +417,9 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID } } } else if isNew && startedState != nil { + if l.spec.Type != nil && *l.spec.Type == apitypes.ListenerTypeBlocks { + return spec, l.es.confirmations.StartConfirmedBlockListener(ctx, l.spec.ID, *l.spec.FromBlock, nil /* new so no checkpoint */, es.batchChannel) + } // Start the new listener - no checkpoint needed here return spec, l.start(startedState, nil) } @@ -500,9 +523,14 @@ func (es *eventStream) Start(ctx context.Context) error { return err } - initialListeners := make([]*ffcapi.EventListenerAddRequest, 0) + initialEventListeners := make([]*ffcapi.EventListenerAddRequest, 0) + initialBlockListeners := make([]*blockListenerAddRequest, 0) for _, l := range es.listeners { - initialListeners = append(initialListeners, l.buildAddRequest(ctx, cp)) + if l.spec.Type != nil && *l.spec.Type == apitypes.ListenerTypeBlocks { + initialBlockListeners = append(initialBlockListeners, l.buildBlockAddRequest(ctx, cp)) + } else { + initialEventListeners = append(initialEventListeners, l.buildAddRequest(ctx, cp)) + } } startedState.blocks, startedState.blockListenerDone = blocklistener.BufferChannel(startedState.ctx, es.confirmations) _, _, err = es.connector.EventStreamStart(startedState.ctx, &ffcapi.EventStreamStartRequest{ @@ -510,7 +538,7 @@ func (es *eventStream) Start(ctx context.Context) error { EventStream: startedState.updates, StreamContext: startedState.ctx, BlockListener: startedState.blocks, - InitialListeners: initialListeners, + InitialListeners: initialEventListeners, }) if err != nil { _ = es.checkSetStatus(ctx, apitypes.EventStreamStatusStarted, apitypes.EventStreamStatusStopped) @@ -522,8 +550,16 @@ func (es *eventStream) Start(ctx context.Context) error { go es.batchLoop(startedState) // Start the confirmations manager - if es.confirmations != nil { - es.confirmations.Start() + es.confirmations.Start() + + // Add all the block listeners + for _, bl := range initialBlockListeners { + // blocks go straight to the batch assembler, as they're already pre-handled by the confirmation manager + if err := es.confirmations.StartConfirmedBlockListener(startedState.ctx, bl.ListenerID, bl.FromBlock, bl.Checkpoint, es.batchChannel); err != nil { + // There are no known reasons for this to fail, as we're starting a fresh set of listeners + log.L(startedState.ctx).Errorf("Failed to start block listener: %s", err) + return err + } } return err @@ -572,9 +608,7 @@ func (es *eventStream) Stop(ctx context.Context) error { } // Stop the confirmations manager - if es.confirmations != nil { - es.confirmations.Stop() - } + es.confirmations.Stop() // Wait for our event loop to stop <-startedState.eventLoopDone @@ -622,7 +656,7 @@ func (es *eventStream) processNewEvent(ctx context.Context, fev *ffcapi.Listener es.mux.Unlock() if l != nil { log.L(ctx).Debugf("%s event detected: %s", l.spec.ID, event) - if es.confirmations == nil { + if es.confirmationsRequired == 0 { // Updates that are just a checkpoint update, go straight to the batch loop. // Or if the confirmation manager is disabled. // - Note this will block the eventLoop when the event stream is blocked @@ -652,7 +686,7 @@ func (es *eventStream) processNewEvent(ctx context.Context, fev *ffcapi.Listener } func (es *eventStream) processRemovedEvent(ctx context.Context, fev *ffcapi.ListenerEvent) { - if fev.Event != nil && fev.Event.ID.ListenerID != nil && es.confirmations != nil { + if fev.Event != nil && fev.Event.ID.ListenerID != nil && es.confirmationsRequired > 0 { err := es.confirmations.Notify(&confirmations.Notification{ NotificationType: confirmations.RemovedEventLog, Event: &confirmations.EventInfo{ @@ -684,6 +718,64 @@ func (es *eventStream) eventLoop(startedState *startedStreamState) { } } +func (es *eventStream) checkConfirmedEventForBatch(e *ffcapi.ListenerEvent) (l *listener, ewc *apitypes.EventWithContext) { + var eToLog fmt.Stringer + var listenerID *fftypes.UUID + switch { + case e.Event != nil: + listenerID = e.Event.ID.ListenerID + eToLog = e.Event + case e.BlockEvent != nil: + listenerID = e.BlockEvent.ListenerID + eToLog = e.BlockEvent + default: + log.L(es.bgCtx).Errorf("Invalid event cannot be dispatched: %+v", e) + return nil, nil + } + es.mux.Lock() + l = es.listeners[*listenerID] + es.mux.Unlock() + if l == nil { + log.L(es.bgCtx).Warnf("Confirmed event not associated with any active listener: %s", eToLog) + return nil, nil + } + currentCheckpoint := l.checkpoint + if currentCheckpoint != nil && !currentCheckpoint.LessThan(e.Checkpoint) { + // This event is behind the current checkpoint - this is a re-detection. + // We're perfectly happy to accept re-detections from the connector, as it can be + // very efficient to batch operations between listeners that cause re-detections. + // However, we need to protect the application from receiving the re-detections. + // This loop is the right place for this check, as we are responsible for writing the checkpoints and + // delivering to the application. So we are the one source of truth. + log.L(es.bgCtx).Debugf("%s '%s' event re-detected behind checkpoint: %s", l.spec.ID, l.spec.SignatureString(), eToLog) + return nil, nil + } + if e.Event != nil { + ewc = &apitypes.EventWithContext{ + StandardContext: apitypes.EventContext{ + StreamID: es.spec.ID, + EthCompatSubID: l.spec.ID, + ListenerName: *l.spec.Name, + ListenerType: apitypes.ListenerTypeEvents, + }, + Event: e.Event, + } + log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.SignatureString(), e.Event) + } else { + ewc = &apitypes.EventWithContext{ + StandardContext: apitypes.EventContext{ + StreamID: es.spec.ID, + EthCompatSubID: l.spec.ID, + ListenerName: *l.spec.Name, + ListenerType: apitypes.ListenerTypeBlocks, + }, + BlockEvent: e.BlockEvent, + } + log.L(es.bgCtx).Debugf("%s '%s' block event confirmed: %s", l.spec.ID, l.spec.SignatureString(), e.Event) + } + return l, ewc +} + // batchLoop receives confirmed events from the confirmation manager, // batches them together, and drives the actions. func (es *eventStream) batchLoop(startedState *startedStreamState) { @@ -706,47 +798,20 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { timedOut := false select { case fev := <-es.batchChannel: - if fev.Event != nil { - es.mux.Lock() - l := es.listeners[*fev.Event.ID.ListenerID] - es.mux.Unlock() - if l != nil { - currentCheckpoint := l.checkpoint - if currentCheckpoint != nil && !currentCheckpoint.LessThan(fev.Checkpoint) { - // This event is behind the current checkpoint - this is a re-detection. - // We're perfectly happy to accept re-detections from the connector, as it can be - // very efficient to batch operations between listeners that cause re-detections. - // However, we need to protect the application from receiving the re-detections. - // This loop is the right place for this check, as we are responsible for writing the checkpoints and - // delivering to the application. So we are the one source of truth. - log.L(es.bgCtx).Debugf("%s '%s' event re-detected behind checkpoint: %s", l.spec.ID, l.spec.SignatureString(), fev.Event) - continue - } - - if batch == nil { - batchNumber++ - batch = &eventStreamBatch{ - number: batchNumber, - timeout: time.NewTimer(time.Duration(*es.spec.BatchTimeout)), - checkpoints: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), - } - } - if fev.Checkpoint != nil { - batch.checkpoints[*fev.Event.ID.ListenerID] = fev.Checkpoint + l, ewc := es.checkConfirmedEventForBatch(fev) + if l != nil && ewc != nil { + if batch == nil { + batchNumber++ + batch = &eventStreamBatch{ + number: batchNumber, + timeout: time.NewTimer(time.Duration(*es.spec.BatchTimeout)), + checkpoints: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), } - - log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.SignatureString(), fev.Event) - batch.events = append(batch.events, &apitypes.EventWithContext{ - StandardContext: apitypes.EventContext{ - StreamID: es.spec.ID, - EthCompatSubID: l.spec.ID, - ListenerName: *l.spec.Name, - }, - Event: *fev.Event, - }) - } else { - log.L(es.bgCtx).Warnf("Confirmed event not associated with any active listener: %s", fev.Event) } + if fev.Checkpoint != nil { + batch.checkpoints[*l.spec.ID] = fev.Checkpoint + } + batch.events = append(batch.events, ewc) } case <-timeoutChannel: timedOut = true @@ -826,7 +891,7 @@ func (es *eventStream) checkUpdateHWMCheckpoint(ctx context.Context, l *listener checkpoint := l.checkpoint inFlight := false - if es.confirmations != nil { + if es.confirmationsRequired > 0 { inFlight = es.confirmations.CheckInFlight(l.spec.ID) } @@ -894,3 +959,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * return true, es.persistence.WriteCheckpoint(startedState.ctx, cp) }) } + +func ptrTo[T any](v T) *T { + return &v +} diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index b005aabe..290d0924 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -99,6 +99,7 @@ func newTestEventStreamWithListener(t *testing.T, mfc *ffcapimocks.API, conf str es = ees.(*eventStream) mcm := &confirmationsmocks.Manager{} es.confirmations = mcm + es.confirmationsRequired = 1 mcm.On("Start").Return(nil).Maybe() mcm.On("Stop").Return(nil).Maybe() mcm.On("Notify", mock.Anything).Run(func(args mock.Arguments) { @@ -409,7 +410,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { batch1 := (<-senderChannel).(*apitypes.EventBatch) assert.Len(t, batch1.Events, 1) assert.Greater(t, batch1.BatchNumber, int64(0)) - assert.Equal(t, "v1", batch1.Events[0].Data.JSONObject().GetString("k1")) + assert.Equal(t, "v1", batch1.Events[0].Event.Data.JSONObject().GetString("k1")) receiverChannel <- &ws.WebSocketCommandMessageOrError{ Msg: &ws.WebSocketCommandMessage{ @@ -426,6 +427,110 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { mfc.AssertExpectations(t) } +func TestWebSocketEventStreamsE2EBlocks(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "websocket": { + "topic": "ut_stream" + } + }`) + + l := &apitypes.Listener{ + ID: apitypes.NewULID(), + Name: strPtr("ut_listener"), + Type: &apitypes.ListenerTypeBlocks, + FromBlock: strPtr(ffcapi.FromBlockLatest), + } + + started := make(chan (chan<- *ffcapi.ListenerEvent), 1) + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) + assert.Empty(t, r.InitialListeners) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + mcm := es.confirmations.(*confirmationsmocks.Manager) + mcm.On("StartConfirmedBlockListener", mock.Anything, l.ID, "latest", mock.MatchedBy(func(cp *ffcapi.BlockListenerCheckpoint) bool { + return cp.Block == 10000 + }), mock.Anything).Run(func(args mock.Arguments) { + started <- args[4].(chan<- *ffcapi.ListenerEvent) + }).Return(nil) + mcm.On("StopConfirmedBlockListener", mock.Anything, l.ID).Return(nil) + + msp := es.persistence.(*persistencemocks.Persistence) + // load existing checkpoint on start + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Time: fftypes.Now(), + Listeners: map[fftypes.UUID]json.RawMessage{ + *l.ID: []byte(`{"block":10000}`), + }, + }, nil) + // write a valid checkpoint + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && string(cp.Listeners[*l.ID]) == `{"block":10001}` + })).Return(nil) + // write a checkpoint when we delete + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID] == nil + })).Return(nil) + + senderChannel, _, receiverChannel := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + assert.Equal(t, apitypes.EventStreamStatusStarted, es.Status()) + + err = es.Start(es.bgCtx) // double start is error + assert.Regexp(t, "FF21027", err) + + r := <-started + + r <- &ffcapi.ListenerEvent{ + Checkpoint: &ffcapi.BlockListenerCheckpoint{Block: 10001}, + BlockEvent: &ffcapi.BlockEvent{ + ListenerID: l.ID, + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(10001), + BlockHash: fftypes.NewRandB32().String(), + ParentHash: fftypes.NewRandB32().String(), + }, + }, + } + + batch1 := (<-senderChannel).(*apitypes.EventBatch) + assert.Len(t, batch1.Events, 1) + assert.Greater(t, batch1.BatchNumber, int64(0)) + assert.Equal(t, int64(10001), batch1.Events[0].BlockEvent.BlockNumber.Int64()) + + receiverChannel <- &ws.WebSocketCommandMessageOrError{ + Msg: &ws.WebSocketCommandMessage{ + Type: "ack", + BatchNumber: batch1.BatchNumber, + }, + } + + err = es.RemoveListener(es.bgCtx, l.ID) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + func TestStartEventStreamCheckpointReadFail(t *testing.T) { es := newTestEventStream(t, `{ @@ -584,7 +689,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { batch1 := <-receivedWebhook assert.Len(t, batch1, 1) - assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) + assert.Equal(t, "v1", batch1[0].Event.Data.JSONObject().GetString("k1")) err = es.Stop(es.bgCtx) assert.NoError(t, err) @@ -1044,6 +1149,121 @@ func TestUpdateAttemptChangeSignature(t *testing.T) { mfc.AssertExpectations(t) } +func TestStartWithExistingBlockListener(t *testing.T) { + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Type: &apitypes.ListenerTypeBlocks, + } + + mfc := &ffcapimocks.API{} + + _, err := newTestEventStreamWithListener(t, mfc, `{ + "name": "ut_stream" + }`, l) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + +func TestStartAndAddBadListenerType(t *testing.T) { + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Type: (*fftypes.FFEnum)(strPtr("wrong")), + } + + mfc := &ffcapimocks.API{} + + es, err := newTestEventStreamWithListener(t, mfc, `{ + "name": "ut_stream" + }`) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.Regexp(t, "FF21089.*wrong", err) + + mfc.AssertExpectations(t) +} + +func TestStartWithBlockListenerFailBeforeStart(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "websocket": { + "topic": "ut_stream" + } + }`) + + l := &apitypes.Listener{ + ID: apitypes.NewULID(), + Name: strPtr("ut_listener"), + Type: &apitypes.ListenerTypeBlocks, + FromBlock: strPtr(ffcapi.FromBlockLatest), + } + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + mcm := es.confirmations.(*confirmationsmocks.Manager) + mcm.On("StartConfirmedBlockListener", mock.Anything, l.ID, "latest", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.Regexp(t, "pop", err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + +func TestAddBlockListenerFailAfterStart(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "websocket": { + "topic": "ut_stream" + } + }`) + + l := &apitypes.Listener{ + ID: apitypes.NewULID(), + Name: strPtr("ut_listener"), + Type: &apitypes.ListenerTypeBlocks, + FromBlock: strPtr(ffcapi.FromBlockLatest), + } + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + mcm := es.confirmations.(*confirmationsmocks.Manager) + mcm.On("StartConfirmedBlockListener", mock.Anything, l.ID, "latest", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.Regexp(t, "pop", err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + func TestAttemptResetNonExistentListener(t *testing.T) { es := newTestEventStream(t, `{ @@ -1296,7 +1516,7 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { } batch1 := (<-broadcastChannel).(*apitypes.EventBatch) assert.Len(t, batch1.Events, 1) - assert.Equal(t, "v1", batch1.Events[0].Data.JSONObject().GetString("k1")) + assert.Equal(t, "v1", batch1.Events[0].Event.Data.JSONObject().GetString("k1")) <-r.StreamContext.Done() <-done @@ -1521,6 +1741,7 @@ func TestEventLoopProcessRemovedEvent(t *testing.T) { ss.cancelCtx() }) es.confirmations = mcm + es.confirmationsRequired = 1 es.listeners[*u1.Event.ID.ListenerID] = &listener{ spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, } @@ -1557,6 +1778,7 @@ func TestEventLoopProcessRemovedEventFail(t *testing.T) { ss.cancelCtx() }) es.confirmations = mcm + es.confirmationsRequired = 1 es.listeners[*u1.Event.ID.ListenerID] = &listener{ spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, } @@ -1589,6 +1811,7 @@ func TestEventLoopConfirmationsManagerBypass(t *testing.T) { }, } es.confirmations = nil + es.confirmationsRequired = 0 es.listeners[*u1.Event.ID.ListenerID] = &listener{ spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, } @@ -1628,6 +1851,7 @@ func TestEventLoopConfirmationsManagerFail(t *testing.T) { ss.cancelCtx() }) es.confirmations = mcm + es.confirmationsRequired = 1 es.listeners[*u1.Event.ID.ListenerID] = &listener{ spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, } @@ -1659,7 +1883,7 @@ func TestSkipEventsBehindCheckpointAndUnknownListener(t *testing.T) { batchLoopDone: make(chan struct{}), action: func(ctx context.Context, batchNumber int64, attempt int, events []*apitypes.EventWithContext) error { assert.Len(t, events, 1) - assert.Equal(t, events[0].ID.BlockNumber.Uint64(), uint64(2001)) + assert.Equal(t, events[0].Event.ID.BlockNumber.Uint64(), uint64(2001)) return nil }, } @@ -1725,6 +1949,7 @@ func TestHWMCheckpointAfterInactivity(t *testing.T) { mcm := &confirmationsmocks.Manager{} mcm.On("CheckInFlight", li.spec.ID).Return(false) es.confirmations = mcm + es.confirmationsRequired = 1 es.listeners[*li.spec.ID] = li mfc := es.connector.(*ffcapimocks.API) @@ -1771,6 +1996,7 @@ func TestHWMCheckpointInFlightSkip(t *testing.T) { ss.cancelCtx() }).Return(true) es.confirmations = mcm + es.confirmationsRequired = 1 es.listeners[*li.spec.ID] = li msp := es.persistence.(*persistencemocks.Persistence) @@ -1805,6 +2031,7 @@ func TestHWMCheckpointFail(t *testing.T) { mcm := &confirmationsmocks.Manager{} mcm.On("CheckInFlight", li.spec.ID).Return(false) es.confirmations = mcm + es.confirmationsRequired = 1 es.listeners[*li.spec.ID] = li mfc := es.connector.(*ffcapimocks.API) @@ -1827,3 +2054,30 @@ func TestHWMCheckpointFail(t *testing.T) { msp.AssertExpectations(t) mcm.AssertExpectations(t) } + +func TestCheckConfirmedEventForBatchIgnoreInvalid(t *testing.T) { + + es := newTestEventStream(t, `{"name": "ut_stream"}`) + + l, ewc := es.checkConfirmedEventForBatch(&ffcapi.ListenerEvent{}) + assert.Nil(t, l) + assert.Nil(t, ewc) +} + +func TestBuildBlockAddREquestBadCheckpoint(t *testing.T) { + + spec := &apitypes.Listener{ + ID: apitypes.NewULID(), + Name: strPtr("ut_listener"), + Type: &apitypes.ListenerTypeBlocks, + FromBlock: strPtr(ffcapi.FromBlockLatest), + } + l := &listener{spec: spec} + + blar := l.buildBlockAddRequest(context.Background(), &apitypes.EventStreamCheckpoint{ + Listeners: apitypes.CheckpointListeners{ + *spec.ID: json.RawMessage([]byte("!!wrong")), + }, + }) + assert.Nil(t, blar.Checkpoint) +} diff --git a/internal/events/listener.go b/internal/events/listener.go index 6e4aa75f..3aaf208e 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -33,6 +33,14 @@ type listener struct { checkpoint ffcapi.EventListenerCheckpoint } +type blockListenerAddRequest struct { + ListenerID *fftypes.UUID + StreamID *fftypes.UUID + Name string + FromBlock string + Checkpoint *ffcapi.BlockListenerCheckpoint +} + func listenerSpecToOptions(spec *apitypes.Listener) ffcapi.EventListenerOptions { return ffcapi.EventListenerOptions{ FromBlock: *spec.FromBlock, @@ -41,12 +49,17 @@ func listenerSpecToOptions(spec *apitypes.Listener) ffcapi.EventListenerOptions } } -func (l *listener) stop(startedState *startedStreamState) error { - _, _, err := l.es.connector.EventListenerRemove(startedState.ctx, &ffcapi.EventListenerRemoveRequest{ - StreamID: l.spec.StreamID, - ListenerID: l.spec.ID, - }) - return err +func (l *listener) stop(startedState *startedStreamState) (err error) { + if l.spec.Type != nil && *l.spec.Type == apitypes.ListenerTypeBlocks { + err = l.es.confirmations.StopConfirmedBlockListener(startedState.ctx, l.spec.ID) + } else { + _, _, err = l.es.connector.EventListenerRemove(startedState.ctx, &ffcapi.EventListenerRemoveRequest{ + StreamID: l.spec.StreamID, + ListenerID: l.spec.ID, + }) + + } + return } func (l *listener) buildAddRequest(ctx context.Context, cp *apitypes.EventStreamCheckpoint) *ffcapi.EventListenerAddRequest { @@ -71,6 +84,28 @@ func (l *listener) buildAddRequest(ctx context.Context, cp *apitypes.EventStream return req } +func (l *listener) buildBlockAddRequest(ctx context.Context, cp *apitypes.EventStreamCheckpoint) *blockListenerAddRequest { + req := &blockListenerAddRequest{ + Name: *l.spec.Name, + ListenerID: l.spec.ID, + StreamID: l.spec.StreamID, + FromBlock: *l.spec.FromBlock, + } + if cp != nil { + jsonCP := cp.Listeners[*l.spec.ID] + if jsonCP != nil { + var listenerCheckpoint ffcapi.BlockListenerCheckpoint + err := json.Unmarshal(jsonCP, &listenerCheckpoint) + if err != nil { + log.L(ctx).Errorf("Failed to restore checkpoint for block listener '%s': %s", l.spec.ID, err) + } else { + req.Checkpoint = &listenerCheckpoint + } + } + } + return req +} + func (l *listener) start(startedState *startedStreamState, cp *apitypes.EventStreamCheckpoint) error { _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, l.buildAddRequest(startedState.ctx, cp)) return err diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go index 77101bab..83f03bc1 100644 --- a/internal/events/webhooks.go +++ b/internal/events/webhooks.go @@ -19,6 +19,7 @@ package events import ( "context" "crypto/tls" + "io" "net" "net/url" "time" @@ -103,12 +104,10 @@ func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber int64, att if w.isAddressBlocked(addr) { return i18n.NewError(ctx, tmmsgs.MsgBlockWebhookAddress, addr, u.Hostname()) } - var resBody []byte req := w.client.R(). SetContext(ctx). SetBody(events). - SetResult(&resBody). - SetError(&resBody) + SetDoNotParseResponse(true) req.Header.Set("Content-Type", "application/json") for h, v := range w.spec.Headers { req.Header.Set(h, v) @@ -118,7 +117,9 @@ func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber int64, att log.L(ctx).Errorf("Webhook %s (%s) batch=%d attempt=%d: %s", *w.spec.URL, u, batchNumber, attempt, err) return i18n.NewError(ctx, tmmsgs.MsgWebhookErr, err) } + defer res.RawBody().Close() if res.IsError() { + resBody, _ := io.ReadAll(res.RawBody()) log.L(ctx).Errorf("Webhook %s (%s) [%d] batch=%d attempt=%d: %s", *w.spec.URL, u, res.StatusCode(), batchNumber, attempt, resBody) err = i18n.NewError(ctx, tmmsgs.MsgWebhookFailedStatus, res.StatusCode()) } diff --git a/internal/persistence/dbmigration/migration_test.go b/internal/persistence/dbmigration/migration_test.go index ecde1388..24cb49f7 100644 --- a/internal/persistence/dbmigration/migration_test.go +++ b/internal/persistence/dbmigration/migration_test.go @@ -53,8 +53,10 @@ func TestDBMigrationOK(t *testing.T) { mdb2.On("WriteListener", mock.Anything, l).Return(nil) tx := &apitypes.TXWithStatus{ - ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, - Receipt: &ffcapi.TransactionReceiptResponse{BlockHash: fftypes.NewRandB32().String()}, + ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, + Receipt: &ffcapi.TransactionReceiptResponse{ + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{BlockHash: fftypes.NewRandB32().String()}, + }, Confirmations: []*apitypes.Confirmation{{BlockHash: fftypes.NewRandB32().String()}}, } mdb1.On("ListTransactionsByCreateTime", mock.Anything, (*apitypes.ManagedTX)(nil), paginationLimit, txhandler.SortDirectionAscending).Return([]*apitypes.ManagedTX{tx.ManagedTX}, nil) @@ -287,8 +289,10 @@ func TestMigrateTransactionsFailWriteConfirmations(t *testing.T) { mdb2 := persistencemocks.NewPersistence(t) tx := &apitypes.TXWithStatus{ - ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, - Receipt: &ffcapi.TransactionReceiptResponse{BlockHash: fftypes.NewRandB32().String()}, + ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, + Receipt: &ffcapi.TransactionReceiptResponse{ + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{BlockHash: fftypes.NewRandB32().String()}, + }, Confirmations: []*apitypes.Confirmation{{BlockHash: fftypes.NewRandB32().String()}}, } mdb1.On("ListTransactionsByCreateTime", mock.Anything, (*apitypes.ManagedTX)(nil), paginationLimit, txhandler.SortDirectionAscending).Return([]*apitypes.ManagedTX{tx.ManagedTX}, nil) @@ -314,8 +318,10 @@ func TestMigrateTransactionsFailWriteReceipt(t *testing.T) { mdb2 := persistencemocks.NewPersistence(t) tx := &apitypes.TXWithStatus{ - ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, - Receipt: &ffcapi.TransactionReceiptResponse{BlockHash: fftypes.NewRandB32().String()}, + ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, + Receipt: &ffcapi.TransactionReceiptResponse{ + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{BlockHash: fftypes.NewRandB32().String()}, + }, Confirmations: []*apitypes.Confirmation{{BlockHash: fftypes.NewRandB32().String()}}, } mdb1.On("ListTransactionsByCreateTime", mock.Anything, (*apitypes.ManagedTX)(nil), paginationLimit, txhandler.SortDirectionAscending).Return([]*apitypes.ManagedTX{tx.ManagedTX}, nil) @@ -340,8 +346,10 @@ func TestMigrateTransactionsFailWriteTx(t *testing.T) { mdb2 := persistencemocks.NewPersistence(t) tx := &apitypes.TXWithStatus{ - ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, - Receipt: &ffcapi.TransactionReceiptResponse{BlockHash: fftypes.NewRandB32().String()}, + ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, + Receipt: &ffcapi.TransactionReceiptResponse{ + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{BlockHash: fftypes.NewRandB32().String()}, + }, Confirmations: []*apitypes.Confirmation{{BlockHash: fftypes.NewRandB32().String()}}, } mdb1.On("ListTransactionsByCreateTime", mock.Anything, (*apitypes.ManagedTX)(nil), paginationLimit, txhandler.SortDirectionAscending).Return([]*apitypes.ManagedTX{tx.ManagedTX}, nil) @@ -365,8 +373,10 @@ func TestMigrateTransactionsFailCheckExists(t *testing.T) { mdb2 := persistencemocks.NewPersistence(t) tx := &apitypes.TXWithStatus{ - ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, - Receipt: &ffcapi.TransactionReceiptResponse{BlockHash: fftypes.NewRandB32().String()}, + ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, + Receipt: &ffcapi.TransactionReceiptResponse{ + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{BlockHash: fftypes.NewRandB32().String()}, + }, Confirmations: []*apitypes.Confirmation{{BlockHash: fftypes.NewRandB32().String()}}, } mdb1.On("ListTransactionsByCreateTime", mock.Anything, (*apitypes.ManagedTX)(nil), paginationLimit, txhandler.SortDirectionAscending).Return([]*apitypes.ManagedTX{tx.ManagedTX}, nil) @@ -389,8 +399,10 @@ func TestMigrateTransactionsFailGetDetail(t *testing.T) { mdb2 := persistencemocks.NewPersistence(t) tx := &apitypes.TXWithStatus{ - ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, - Receipt: &ffcapi.TransactionReceiptResponse{BlockHash: fftypes.NewRandB32().String()}, + ManagedTX: &apitypes.ManagedTX{ID: fftypes.NewUUID().String()}, + Receipt: &ffcapi.TransactionReceiptResponse{ + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{BlockHash: fftypes.NewRandB32().String()}, + }, Confirmations: []*apitypes.Confirmation{{BlockHash: fftypes.NewRandB32().String()}}, } mdb1.On("ListTransactionsByCreateTime", mock.Anything, (*apitypes.ManagedTX)(nil), paginationLimit, txhandler.SortDirectionAscending).Return([]*apitypes.ManagedTX{tx.ManagedTX}, nil) diff --git a/internal/persistence/leveldb/leveldb_persistence_test.go b/internal/persistence/leveldb/leveldb_persistence_test.go index 5f62ce96..0e529097 100644 --- a/internal/persistence/leveldb/leveldb_persistence_test.go +++ b/internal/persistence/leveldb/leveldb_persistence_test.go @@ -1006,7 +1006,7 @@ func TestSetReceipt(t *testing.T) { mtx := newTestTX("0x12345", apitypes.TxStatusPending) receipt := &ffcapi.TransactionReceiptResponse{ - Success: true, + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{Success: true}, } err := p.SetTransactionReceipt(ctx, mtx.ID, receipt) diff --git a/internal/persistence/postgres/listeners.go b/internal/persistence/postgres/listeners.go index cd4289f2..4a2992a4 100644 --- a/internal/persistence/postgres/listeners.go +++ b/internal/persistence/postgres/listeners.go @@ -36,6 +36,7 @@ func (p *sqlPersistence) newListenersCollection(forMigration bool) *dbsql.CrudBa dbsql.ColumnCreated, dbsql.ColumnUpdated, "name", + "type", "stream_id", "filters", "options", @@ -58,6 +59,8 @@ func (p *sqlPersistence) newListenersCollection(forMigration bool) *dbsql.CrudBa return &inst.Created case dbsql.ColumnUpdated: return &inst.Updated + case "type": + return &inst.Type case "name": return &inst.Name case "stream_id": diff --git a/internal/persistence/postgres/listeners_test.go b/internal/persistence/postgres/listeners_test.go index c1562c33..7bafbfe2 100644 --- a/internal/persistence/postgres/listeners_test.go +++ b/internal/persistence/postgres/listeners_test.go @@ -40,6 +40,7 @@ func TestListenerBasicPSQL(t *testing.T) { ID: fftypes.NewUUID(), Name: strPtr("l1"), StreamID: stream1, + Type: &apitypes.ListenerTypeEvents, Filters: apitypes.ListenerFilters{ *fftypes.JSONAnyPtr(`{"filter":"one"}`), *fftypes.JSONAnyPtr(`{"filter":"two"}`), @@ -66,6 +67,7 @@ func TestListenerBasicPSQL(t *testing.T) { lUpdated := &apitypes.Listener{ ID: l.ID, Name: strPtr("l2"), + Type: &apitypes.ListenerTypeEvents, StreamID: stream1, Filters: apitypes.ListenerFilters{ *fftypes.JSONAnyPtr(`{"filter":"three"}`), @@ -111,6 +113,7 @@ func TestListenerAfterPaginatePSQL(t *testing.T) { l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: strPtr(fmt.Sprintf("l_%.3d", i)), + Type: &apitypes.ListenerTypeEvents, StreamID: stream1, } if i >= 10 { diff --git a/internal/persistence/postgres/receipts_test.go b/internal/persistence/postgres/receipts_test.go index b0616f15..707f39a4 100644 --- a/internal/persistence/postgres/receipts_test.go +++ b/internal/persistence/postgres/receipts_test.go @@ -52,13 +52,17 @@ func TestReceiptsInsertTwoInOneCyclePSQL(t *testing.T) { // Insert receipt err := p.SetTransactionReceipt(ctx, tx1ID, &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + }, }) assert.NoError(t, err) // Immediately replace it in the same cycle err = p.SetTransactionReceipt(ctx, tx1ID, &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(23456), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(23456), + }, }) assert.NoError(t, err) @@ -98,13 +102,17 @@ func TestReceiptsReplaceInAnotherCyclePSQL(t *testing.T) { // Insert receipt err := p.SetTransactionReceipt(ctx, tx1ID, &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + }, }) assert.NoError(t, err) // Replace it in the next cycle (note ConfigTXWriterBatchSize at head of test) err = p.SetTransactionReceipt(ctx, tx1ID, &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(23456), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(23456), + }, }) assert.NoError(t, err) diff --git a/internal/persistence/postgres/transactions_test.go b/internal/persistence/postgres/transactions_test.go index 424560e4..9a4c187a 100644 --- a/internal/persistence/postgres/transactions_test.go +++ b/internal/persistence/postgres/transactions_test.go @@ -79,13 +79,15 @@ func TestTransactionBasicValidationPSQL(t *testing.T) { // A receipt receipt := &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(111111), - TransactionIndex: fftypes.NewFFBigInt(222222), - BlockHash: "0x333333", - Success: true, - ProtocolID: "000/111/222", - ExtraInfo: fftypes.JSONAnyPtr(`{"extra":"444444"}`), - ContractLocation: fftypes.JSONAnyPtr(`{"address":"0x555555"}`), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(111111), + TransactionIndex: fftypes.NewFFBigInt(222222), + BlockHash: "0x333333", + Success: true, + ProtocolID: "000/111/222", + ExtraInfo: fftypes.JSONAnyPtr(`{"extra":"444444"}`), + ContractLocation: fftypes.JSONAnyPtr(`{"address":"0x555555"}`), + }, } err = p.SetTransactionReceipt(ctx, txID, receipt) assert.NoError(t, err) diff --git a/internal/tmmsgs/en_error_messages.go b/internal/tmmsgs/en_error_messages.go index 9295d23f..3d358316 100644 --- a/internal/tmmsgs/en_error_messages.go +++ b/internal/tmmsgs/en_error_messages.go @@ -101,4 +101,8 @@ var ( MsgTransactionPersistenceError = ffe("FF21084", "Failed to persist transaction data", 500) MsgOpNotSupportedWithoutRichQuery = ffe("FF21085", "Not supported: The connector must be configured with a rich query database to support this operation", 501) MsgTransactionOpInvalid = ffe("FF21086", "Transaction operation is missing required fields", 400) + MsgBlockListenerAlreadyStarted = ffe("FF21087", "Block listener %s is already started", http.StatusConflict) + MsgBlockListenerNotStarted = ffe("FF21088", "Block listener %s not started", http.StatusConflict) + MsgBadListenerType = ffe("FF21089", "Invalid listener type: %s", http.StatusBadRequest) + MsgFromBlockInvalid = ffe("FF21090", "From block invalid. Must be 'earliest', 'latest' or a decimal: %s", http.StatusBadRequest) ) diff --git a/mocks/apiclientmocks/fftm_client.go b/mocks/apiclientmocks/fftm_client.go index a24f0d57..593aaa44 100644 --- a/mocks/apiclientmocks/fftm_client.go +++ b/mocks/apiclientmocks/fftm_client.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package apiclientmocks @@ -19,6 +19,10 @@ type FFTMClient struct { func (_m *FFTMClient) DeleteEventStream(ctx context.Context, eventStreamID string) error { ret := _m.Called(ctx, eventStreamID) + if len(ret) == 0 { + panic("no return value specified for DeleteEventStream") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, eventStreamID) @@ -33,6 +37,10 @@ func (_m *FFTMClient) DeleteEventStream(ctx context.Context, eventStreamID strin func (_m *FFTMClient) DeleteEventStreamsByName(ctx context.Context, nameRegex string) error { ret := _m.Called(ctx, nameRegex) + if len(ret) == 0 { + panic("no return value specified for DeleteEventStreamsByName") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, nameRegex) @@ -47,6 +55,10 @@ func (_m *FFTMClient) DeleteEventStreamsByName(ctx context.Context, nameRegex st func (_m *FFTMClient) DeleteListener(ctx context.Context, eventStreamID string, listenerID string) error { ret := _m.Called(ctx, eventStreamID, listenerID) + if len(ret) == 0 { + panic("no return value specified for DeleteListener") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { r0 = rf(ctx, eventStreamID, listenerID) @@ -61,6 +73,10 @@ func (_m *FFTMClient) DeleteListener(ctx context.Context, eventStreamID string, func (_m *FFTMClient) DeleteListenersByName(ctx context.Context, eventStreamID string, nameRegex string) error { ret := _m.Called(ctx, eventStreamID, nameRegex) + if len(ret) == 0 { + panic("no return value specified for DeleteListenersByName") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { r0 = rf(ctx, eventStreamID, nameRegex) @@ -75,6 +91,10 @@ func (_m *FFTMClient) DeleteListenersByName(ctx context.Context, eventStreamID s func (_m *FFTMClient) GetEventStreams(ctx context.Context) ([]apitypes.EventStream, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for GetEventStreams") + } + var r0 []apitypes.EventStream var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]apitypes.EventStream, error)); ok { @@ -101,6 +121,10 @@ func (_m *FFTMClient) GetEventStreams(ctx context.Context) ([]apitypes.EventStre func (_m *FFTMClient) GetListeners(ctx context.Context, eventStreamID string) ([]apitypes.Listener, error) { ret := _m.Called(ctx, eventStreamID) + if len(ret) == 0 { + panic("no return value specified for GetListeners") + } + var r0 []apitypes.Listener var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) ([]apitypes.Listener, error)); ok { diff --git a/mocks/confirmationsmocks/manager.go b/mocks/confirmationsmocks/manager.go index c353a8dd..ea170362 100644 --- a/mocks/confirmationsmocks/manager.go +++ b/mocks/confirmationsmocks/manager.go @@ -1,9 +1,12 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package confirmationsmocks import ( + context "context" + confirmations "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -20,6 +23,10 @@ type Manager struct { func (_m *Manager) CheckInFlight(listenerID *fftypes.UUID) bool { ret := _m.Called(listenerID) + if len(ret) == 0 { + panic("no return value specified for CheckInFlight") + } + var r0 bool if rf, ok := ret.Get(0).(func(*fftypes.UUID) bool); ok { r0 = rf(listenerID) @@ -34,6 +41,10 @@ func (_m *Manager) CheckInFlight(listenerID *fftypes.UUID) bool { func (_m *Manager) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for NewBlockHashes") + } + var r0 chan<- *ffcapi.BlockHashEvent if rf, ok := ret.Get(0).(func() chan<- *ffcapi.BlockHashEvent); ok { r0 = rf() @@ -50,6 +61,10 @@ func (_m *Manager) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { func (_m *Manager) Notify(n *confirmations.Notification) error { ret := _m.Called(n) + if len(ret) == 0 { + panic("no return value specified for Notify") + } + var r0 error if rf, ok := ret.Get(0).(func(*confirmations.Notification) error); ok { r0 = rf(n) @@ -65,11 +80,47 @@ func (_m *Manager) Start() { _m.Called() } +// StartConfirmedBlockListener provides a mock function with given fields: ctx, id, fromBlock, checkpoint, eventStream +func (_m *Manager) StartConfirmedBlockListener(ctx context.Context, id *fftypes.UUID, fromBlock string, checkpoint *ffcapi.BlockListenerCheckpoint, eventStream chan<- *ffcapi.ListenerEvent) error { + ret := _m.Called(ctx, id, fromBlock, checkpoint, eventStream) + + if len(ret) == 0 { + panic("no return value specified for StartConfirmedBlockListener") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, string, *ffcapi.BlockListenerCheckpoint, chan<- *ffcapi.ListenerEvent) error); ok { + r0 = rf(ctx, id, fromBlock, checkpoint, eventStream) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Stop provides a mock function with given fields: func (_m *Manager) Stop() { _m.Called() } +// StopConfirmedBlockListener provides a mock function with given fields: ctx, id +func (_m *Manager) StopConfirmedBlockListener(ctx context.Context, id *fftypes.UUID) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for StopConfirmedBlockListener") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewManager(t interface { diff --git a/mocks/eventsmocks/stream.go b/mocks/eventsmocks/stream.go index 05c4f1e4..7987f17f 100644 --- a/mocks/eventsmocks/stream.go +++ b/mocks/eventsmocks/stream.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package eventsmocks @@ -21,6 +21,10 @@ type Stream struct { func (_m *Stream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { ret := _m.Called(ctx, id, updates, reset) + if len(ret) == 0 { + panic("no return value specified for AddOrUpdateListener") + } + var r0 *apitypes.Listener var r1 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, *apitypes.Listener, bool) (*apitypes.Listener, error)); ok { @@ -47,6 +51,10 @@ func (_m *Stream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, upd func (_m *Stream) Delete(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Delete") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -61,6 +69,10 @@ func (_m *Stream) Delete(ctx context.Context) error { func (_m *Stream) RemoveListener(ctx context.Context, id *fftypes.UUID) error { ret := _m.Called(ctx, id) + if len(ret) == 0 { + panic("no return value specified for RemoveListener") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { r0 = rf(ctx, id) @@ -75,6 +87,10 @@ func (_m *Stream) RemoveListener(ctx context.Context, id *fftypes.UUID) error { func (_m *Stream) Spec() *apitypes.EventStream { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Spec") + } + var r0 *apitypes.EventStream if rf, ok := ret.Get(0).(func() *apitypes.EventStream); ok { r0 = rf() @@ -91,6 +107,10 @@ func (_m *Stream) Spec() *apitypes.EventStream { func (_m *Stream) Start(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Start") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -105,6 +125,10 @@ func (_m *Stream) Start(ctx context.Context) error { func (_m *Stream) Status() apitypes.EventStreamStatus { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Status") + } + var r0 apitypes.EventStreamStatus if rf, ok := ret.Get(0).(func() apitypes.EventStreamStatus); ok { r0 = rf() @@ -119,6 +143,10 @@ func (_m *Stream) Status() apitypes.EventStreamStatus { func (_m *Stream) Stop(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Stop") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -133,6 +161,10 @@ func (_m *Stream) Stop(ctx context.Context) error { func (_m *Stream) UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error { ret := _m.Called(ctx, updates) + if len(ret) == 0 { + panic("no return value specified for UpdateSpec") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStream) error); ok { r0 = rf(ctx, updates) diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index 17cccc8c..3c48878a 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package ffcapimocks @@ -18,6 +18,10 @@ type API struct { func (_m *API) AddressBalance(ctx context.Context, req *ffcapi.AddressBalanceRequest) (*ffcapi.AddressBalanceResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for AddressBalance") + } + var r0 *ffcapi.AddressBalanceResponse var r1 ffcapi.ErrorReason var r2 error @@ -51,6 +55,10 @@ func (_m *API) AddressBalance(ctx context.Context, req *ffcapi.AddressBalanceReq func (_m *API) BlockInfoByHash(ctx context.Context, req *ffcapi.BlockInfoByHashRequest) (*ffcapi.BlockInfoByHashResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for BlockInfoByHash") + } + var r0 *ffcapi.BlockInfoByHashResponse var r1 ffcapi.ErrorReason var r2 error @@ -84,6 +92,10 @@ func (_m *API) BlockInfoByHash(ctx context.Context, req *ffcapi.BlockInfoByHashR func (_m *API) BlockInfoByNumber(ctx context.Context, req *ffcapi.BlockInfoByNumberRequest) (*ffcapi.BlockInfoByNumberResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for BlockInfoByNumber") + } + var r0 *ffcapi.BlockInfoByNumberResponse var r1 ffcapi.ErrorReason var r2 error @@ -117,6 +129,10 @@ func (_m *API) BlockInfoByNumber(ctx context.Context, req *ffcapi.BlockInfoByNum func (_m *API) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for DeployContractPrepare") + } + var r0 *ffcapi.TransactionPrepareResponse var r1 ffcapi.ErrorReason var r2 error @@ -150,6 +166,10 @@ func (_m *API) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDe func (_m *API) EventListenerAdd(ctx context.Context, req *ffcapi.EventListenerAddRequest) (*ffcapi.EventListenerAddResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for EventListenerAdd") + } + var r0 *ffcapi.EventListenerAddResponse var r1 ffcapi.ErrorReason var r2 error @@ -183,6 +203,10 @@ func (_m *API) EventListenerAdd(ctx context.Context, req *ffcapi.EventListenerAd func (_m *API) EventListenerHWM(ctx context.Context, req *ffcapi.EventListenerHWMRequest) (*ffcapi.EventListenerHWMResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for EventListenerHWM") + } + var r0 *ffcapi.EventListenerHWMResponse var r1 ffcapi.ErrorReason var r2 error @@ -216,6 +240,10 @@ func (_m *API) EventListenerHWM(ctx context.Context, req *ffcapi.EventListenerHW func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListenerRemoveRequest) (*ffcapi.EventListenerRemoveResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for EventListenerRemove") + } + var r0 *ffcapi.EventListenerRemoveResponse var r1 ffcapi.ErrorReason var r2 error @@ -249,6 +277,10 @@ func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListene func (_m *API) EventListenerVerifyOptions(ctx context.Context, req *ffcapi.EventListenerVerifyOptionsRequest) (*ffcapi.EventListenerVerifyOptionsResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for EventListenerVerifyOptions") + } + var r0 *ffcapi.EventListenerVerifyOptionsResponse var r1 ffcapi.ErrorReason var r2 error @@ -282,6 +314,10 @@ func (_m *API) EventListenerVerifyOptions(ctx context.Context, req *ffcapi.Event func (_m *API) EventStreamNewCheckpointStruct() ffcapi.EventListenerCheckpoint { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for EventStreamNewCheckpointStruct") + } + var r0 ffcapi.EventListenerCheckpoint if rf, ok := ret.Get(0).(func() ffcapi.EventListenerCheckpoint); ok { r0 = rf() @@ -298,6 +334,10 @@ func (_m *API) EventStreamNewCheckpointStruct() ffcapi.EventListenerCheckpoint { func (_m *API) EventStreamStart(ctx context.Context, req *ffcapi.EventStreamStartRequest) (*ffcapi.EventStreamStartResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for EventStreamStart") + } + var r0 *ffcapi.EventStreamStartResponse var r1 ffcapi.ErrorReason var r2 error @@ -331,6 +371,10 @@ func (_m *API) EventStreamStart(ctx context.Context, req *ffcapi.EventStreamStar func (_m *API) EventStreamStopped(ctx context.Context, req *ffcapi.EventStreamStoppedRequest) (*ffcapi.EventStreamStoppedResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for EventStreamStopped") + } + var r0 *ffcapi.EventStreamStoppedResponse var r1 ffcapi.ErrorReason var r2 error @@ -364,6 +408,10 @@ func (_m *API) EventStreamStopped(ctx context.Context, req *ffcapi.EventStreamSt func (_m *API) GasEstimate(ctx context.Context, req *ffcapi.TransactionInput) (*ffcapi.GasEstimateResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for GasEstimate") + } + var r0 *ffcapi.GasEstimateResponse var r1 ffcapi.ErrorReason var r2 error @@ -397,6 +445,10 @@ func (_m *API) GasEstimate(ctx context.Context, req *ffcapi.TransactionInput) (* func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimateRequest) (*ffcapi.GasPriceEstimateResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for GasPriceEstimate") + } + var r0 *ffcapi.GasPriceEstimateResponse var r1 ffcapi.ErrorReason var r2 error @@ -430,6 +482,10 @@ func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimat func (_m *API) IsLive(ctx context.Context) (*ffcapi.LiveResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for IsLive") + } + var r0 *ffcapi.LiveResponse var r1 ffcapi.ErrorReason var r2 error @@ -463,6 +519,10 @@ func (_m *API) IsLive(ctx context.Context) (*ffcapi.LiveResponse, ffcapi.ErrorRe func (_m *API) IsReady(ctx context.Context) (*ffcapi.ReadyResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for IsReady") + } + var r0 *ffcapi.ReadyResponse var r1 ffcapi.ErrorReason var r2 error @@ -496,6 +556,10 @@ func (_m *API) IsReady(ctx context.Context) (*ffcapi.ReadyResponse, ffcapi.Error func (_m *API) NewBlockListener(ctx context.Context, req *ffcapi.NewBlockListenerRequest) (*ffcapi.NewBlockListenerResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for NewBlockListener") + } + var r0 *ffcapi.NewBlockListenerResponse var r1 ffcapi.ErrorReason var r2 error @@ -529,6 +593,10 @@ func (_m *API) NewBlockListener(ctx context.Context, req *ffcapi.NewBlockListene func (_m *API) NextNonceForSigner(ctx context.Context, req *ffcapi.NextNonceForSignerRequest) (*ffcapi.NextNonceForSignerResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for NextNonceForSigner") + } + var r0 *ffcapi.NextNonceForSignerResponse var r1 ffcapi.ErrorReason var r2 error @@ -562,6 +630,10 @@ func (_m *API) NextNonceForSigner(ctx context.Context, req *ffcapi.NextNonceForS func (_m *API) QueryInvoke(ctx context.Context, req *ffcapi.QueryInvokeRequest) (*ffcapi.QueryInvokeResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for QueryInvoke") + } + var r0 *ffcapi.QueryInvokeResponse var r1 ffcapi.ErrorReason var r2 error @@ -595,6 +667,10 @@ func (_m *API) QueryInvoke(ctx context.Context, req *ffcapi.QueryInvokeRequest) func (_m *API) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for TransactionPrepare") + } + var r0 *ffcapi.TransactionPrepareResponse var r1 ffcapi.ErrorReason var r2 error @@ -628,6 +704,10 @@ func (_m *API) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPr func (_m *API) TransactionReceipt(ctx context.Context, req *ffcapi.TransactionReceiptRequest) (*ffcapi.TransactionReceiptResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for TransactionReceipt") + } + var r0 *ffcapi.TransactionReceiptResponse var r1 ffcapi.ErrorReason var r2 error @@ -661,6 +741,10 @@ func (_m *API) TransactionReceipt(ctx context.Context, req *ffcapi.TransactionRe func (_m *API) TransactionSend(ctx context.Context, req *ffcapi.TransactionSendRequest) (*ffcapi.TransactionSendResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) + if len(ret) == 0 { + panic("no return value specified for TransactionSend") + } + var r0 *ffcapi.TransactionSendResponse var r1 ffcapi.ErrorReason var r2 error diff --git a/mocks/metricsmocks/event_metrics_emitter.go b/mocks/metricsmocks/event_metrics_emitter.go index 744036d6..443b9e58 100644 --- a/mocks/metricsmocks/event_metrics_emitter.go +++ b/mocks/metricsmocks/event_metrics_emitter.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package metricsmocks diff --git a/mocks/metricsmocks/transaction_handler_metrics.go b/mocks/metricsmocks/transaction_handler_metrics.go index 5759b688..f5cb5818 100644 --- a/mocks/metricsmocks/transaction_handler_metrics.go +++ b/mocks/metricsmocks/transaction_handler_metrics.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package metricsmocks diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index bd0a68ab..a0ca43fd 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package persistencemocks @@ -27,6 +27,10 @@ type Persistence struct { func (_m *Persistence) AddSubStatusAction(ctx context.Context, txID string, subStatus apitypes.TxSubStatus, action apitypes.TxAction, info *fftypes.JSONAny, err *fftypes.JSONAny, actionOccurred *fftypes.FFTime) error { ret := _m.Called(ctx, txID, subStatus, action, info, err, actionOccurred) + if len(ret) == 0 { + panic("no return value specified for AddSubStatusAction") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, apitypes.TxSubStatus, apitypes.TxAction, *fftypes.JSONAny, *fftypes.JSONAny, *fftypes.FFTime) error); ok { r0 = rf(ctx, txID, subStatus, action, info, err, actionOccurred) @@ -48,6 +52,10 @@ func (_m *Persistence) AddTransactionConfirmations(ctx context.Context, txID str _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for AddTransactionConfirmations") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, bool, ...*apitypes.Confirmation) error); ok { r0 = rf(ctx, txID, clearExisting, confirmations...) @@ -67,6 +75,10 @@ func (_m *Persistence) Close(ctx context.Context) { func (_m *Persistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { ret := _m.Called(ctx, streamID) + if len(ret) == 0 { + panic("no return value specified for DeleteCheckpoint") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { r0 = rf(ctx, streamID) @@ -81,6 +93,10 @@ func (_m *Persistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.U func (_m *Persistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { ret := _m.Called(ctx, listenerID) + if len(ret) == 0 { + panic("no return value specified for DeleteListener") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { r0 = rf(ctx, listenerID) @@ -95,6 +111,10 @@ func (_m *Persistence) DeleteListener(ctx context.Context, listenerID *fftypes.U func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { ret := _m.Called(ctx, streamID) + if len(ret) == 0 { + panic("no return value specified for DeleteStream") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { r0 = rf(ctx, streamID) @@ -109,6 +129,10 @@ func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) func (_m *Persistence) DeleteTransaction(ctx context.Context, txID string) error { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for DeleteTransaction") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, txID) @@ -123,6 +147,10 @@ func (_m *Persistence) DeleteTransaction(ctx context.Context, txID string) error func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) { ret := _m.Called(ctx, streamID) + if len(ret) == 0 { + panic("no return value specified for GetCheckpoint") + } + var r0 *apitypes.EventStreamCheckpoint var r1 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error)); ok { @@ -149,6 +177,10 @@ func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) { ret := _m.Called(ctx, listenerID) + if len(ret) == 0 { + panic("no return value specified for GetListener") + } + var r0 *apitypes.Listener var r1 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) (*apitypes.Listener, error)); ok { @@ -175,6 +207,10 @@ func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) { ret := _m.Called(ctx, streamID) + if len(ret) == 0 { + panic("no return value specified for GetStream") + } + var r0 *apitypes.EventStream var r1 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) (*apitypes.EventStream, error)); ok { @@ -201,6 +237,10 @@ func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (* func (_m *Persistence) GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for GetTransactionByID") + } + var r0 *apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*apitypes.ManagedTX, error)); ok { @@ -227,6 +267,10 @@ func (_m *Persistence) GetTransactionByID(ctx context.Context, txID string) (*ap func (_m *Persistence) GetTransactionByIDWithStatus(ctx context.Context, txID string, history bool) (*apitypes.TXWithStatus, error) { ret := _m.Called(ctx, txID, history) + if len(ret) == 0 { + panic("no return value specified for GetTransactionByIDWithStatus") + } + var r0 *apitypes.TXWithStatus var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, bool) (*apitypes.TXWithStatus, error)); ok { @@ -253,6 +297,10 @@ func (_m *Persistence) GetTransactionByIDWithStatus(ctx context.Context, txID st func (_m *Persistence) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, signer, nonce) + if len(ret) == 0 { + panic("no return value specified for GetTransactionByNonce") + } + var r0 *apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt) (*apitypes.ManagedTX, error)); ok { @@ -279,6 +327,10 @@ func (_m *Persistence) GetTransactionByNonce(ctx context.Context, signer string, func (_m *Persistence) GetTransactionConfirmations(ctx context.Context, txID string) ([]*apitypes.Confirmation, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for GetTransactionConfirmations") + } + var r0 []*apitypes.Confirmation var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) ([]*apitypes.Confirmation, error)); ok { @@ -305,6 +357,10 @@ func (_m *Persistence) GetTransactionConfirmations(ctx context.Context, txID str func (_m *Persistence) GetTransactionReceipt(ctx context.Context, txID string) (*ffcapi.TransactionReceiptResponse, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for GetTransactionReceipt") + } + var r0 *ffcapi.TransactionReceiptResponse var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*ffcapi.TransactionReceiptResponse, error)); ok { @@ -331,6 +387,10 @@ func (_m *Persistence) GetTransactionReceipt(ctx context.Context, txID string) ( func (_m *Persistence) InsertTransactionPreAssignedNonce(ctx context.Context, tx *apitypes.ManagedTX) error { ret := _m.Called(ctx, tx) + if len(ret) == 0 { + panic("no return value specified for InsertTransactionPreAssignedNonce") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX) error); ok { r0 = rf(ctx, tx) @@ -345,6 +405,10 @@ func (_m *Persistence) InsertTransactionPreAssignedNonce(ctx context.Context, tx func (_m *Persistence) InsertTransactionWithNextNonce(ctx context.Context, tx *apitypes.ManagedTX, lookupNextNonce txhandler.NextNonceCallback) error { ret := _m.Called(ctx, tx, lookupNextNonce) + if len(ret) == 0 { + panic("no return value specified for InsertTransactionWithNextNonce") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, txhandler.NextNonceCallback) error); ok { r0 = rf(ctx, tx, lookupNextNonce) @@ -359,6 +423,10 @@ func (_m *Persistence) InsertTransactionWithNextNonce(ctx context.Context, tx *a func (_m *Persistence) ListListenersByCreateTime(ctx context.Context, after *fftypes.UUID, limit int, dir txhandler.SortDirection) ([]*apitypes.Listener, error) { ret := _m.Called(ctx, after, limit, dir) + if len(ret) == 0 { + panic("no return value specified for ListListenersByCreateTime") + } + var r0 []*apitypes.Listener var r1 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, txhandler.SortDirection) ([]*apitypes.Listener, error)); ok { @@ -385,6 +453,10 @@ func (_m *Persistence) ListListenersByCreateTime(ctx context.Context, after *fft func (_m *Persistence) ListStreamListenersByCreateTime(ctx context.Context, after *fftypes.UUID, limit int, dir txhandler.SortDirection, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { ret := _m.Called(ctx, after, limit, dir, streamID) + if len(ret) == 0 { + panic("no return value specified for ListStreamListenersByCreateTime") + } + var r0 []*apitypes.Listener var r1 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, txhandler.SortDirection, *fftypes.UUID) ([]*apitypes.Listener, error)); ok { @@ -411,6 +483,10 @@ func (_m *Persistence) ListStreamListenersByCreateTime(ctx context.Context, afte func (_m *Persistence) ListStreamsByCreateTime(ctx context.Context, after *fftypes.UUID, limit int, dir txhandler.SortDirection) ([]*apitypes.EventStream, error) { ret := _m.Called(ctx, after, limit, dir) + if len(ret) == 0 { + panic("no return value specified for ListStreamsByCreateTime") + } + var r0 []*apitypes.EventStream var r1 error if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, txhandler.SortDirection) ([]*apitypes.EventStream, error)); ok { @@ -437,6 +513,10 @@ func (_m *Persistence) ListStreamsByCreateTime(ctx context.Context, after *fftyp func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir txhandler.SortDirection) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, after, limit, dir) + if len(ret) == 0 { + panic("no return value specified for ListTransactionsByCreateTime") + } + var r0 []*apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, int, txhandler.SortDirection) ([]*apitypes.ManagedTX, error)); ok { @@ -463,6 +543,10 @@ func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after * func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int, dir txhandler.SortDirection) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, signer, after, limit, dir) + if len(ret) == 0 { + panic("no return value specified for ListTransactionsByNonce") + } + var r0 []*apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt, int, txhandler.SortDirection) ([]*apitypes.ManagedTX, error)); ok { @@ -489,6 +573,10 @@ func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer strin func (_m *Persistence) ListTransactionsPending(ctx context.Context, afterSequenceID string, limit int, dir txhandler.SortDirection) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, afterSequenceID, limit, dir) + if len(ret) == 0 { + panic("no return value specified for ListTransactionsPending") + } + var r0 []*apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, int, txhandler.SortDirection) ([]*apitypes.ManagedTX, error)); ok { @@ -515,6 +603,10 @@ func (_m *Persistence) ListTransactionsPending(ctx context.Context, afterSequenc func (_m *Persistence) RichQuery() persistence.RichQuery { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for RichQuery") + } + var r0 persistence.RichQuery if rf, ok := ret.Get(0).(func() persistence.RichQuery); ok { r0 = rf() @@ -531,6 +623,10 @@ func (_m *Persistence) RichQuery() persistence.RichQuery { func (_m *Persistence) SetTransactionReceipt(ctx context.Context, txID string, receipt *ffcapi.TransactionReceiptResponse) error { ret := _m.Called(ctx, txID, receipt) + if len(ret) == 0 { + panic("no return value specified for SetTransactionReceipt") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.TransactionReceiptResponse) error); ok { r0 = rf(ctx, txID, receipt) @@ -545,6 +641,10 @@ func (_m *Persistence) SetTransactionReceipt(ctx context.Context, txID string, r func (_m *Persistence) UpdateTransaction(ctx context.Context, txID string, updates *apitypes.TXUpdates) error { ret := _m.Called(ctx, txID, updates) + if len(ret) == 0 { + panic("no return value specified for UpdateTransaction") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, *apitypes.TXUpdates) error); ok { r0 = rf(ctx, txID, updates) @@ -559,6 +659,10 @@ func (_m *Persistence) UpdateTransaction(ctx context.Context, txID string, updat func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error { ret := _m.Called(ctx, checkpoint) + if len(ret) == 0 { + panic("no return value specified for WriteCheckpoint") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStreamCheckpoint) error); ok { r0 = rf(ctx, checkpoint) @@ -573,6 +677,10 @@ func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes func (_m *Persistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { ret := _m.Called(ctx, spec) + if len(ret) == 0 { + panic("no return value specified for WriteListener") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.Listener) error); ok { r0 = rf(ctx, spec) @@ -587,6 +695,10 @@ func (_m *Persistence) WriteListener(ctx context.Context, spec *apitypes.Listene func (_m *Persistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { ret := _m.Called(ctx, spec) + if len(ret) == 0 { + panic("no return value specified for WriteStream") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStream) error); ok { r0 = rf(ctx, spec) diff --git a/mocks/persistencemocks/rich_query.go b/mocks/persistencemocks/rich_query.go index c20465f4..f28cb57d 100644 --- a/mocks/persistencemocks/rich_query.go +++ b/mocks/persistencemocks/rich_query.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package persistencemocks @@ -23,6 +23,10 @@ type RichQuery struct { func (_m *RichQuery) ListListeners(ctx context.Context, filter ffapi.AndFilter) ([]*apitypes.Listener, *ffapi.FilterResult, error) { ret := _m.Called(ctx, filter) + if len(ret) == 0 { + panic("no return value specified for ListListeners") + } + var r0 []*apitypes.Listener var r1 *ffapi.FilterResult var r2 error @@ -58,6 +62,10 @@ func (_m *RichQuery) ListListeners(ctx context.Context, filter ffapi.AndFilter) func (_m *RichQuery) ListStreamListeners(ctx context.Context, streamID *fftypes.UUID, filter ffapi.AndFilter) ([]*apitypes.Listener, *ffapi.FilterResult, error) { ret := _m.Called(ctx, streamID, filter) + if len(ret) == 0 { + panic("no return value specified for ListStreamListeners") + } + var r0 []*apitypes.Listener var r1 *ffapi.FilterResult var r2 error @@ -93,6 +101,10 @@ func (_m *RichQuery) ListStreamListeners(ctx context.Context, streamID *fftypes. func (_m *RichQuery) ListStreams(ctx context.Context, filter ffapi.AndFilter) ([]*apitypes.EventStream, *ffapi.FilterResult, error) { ret := _m.Called(ctx, filter) + if len(ret) == 0 { + panic("no return value specified for ListStreams") + } + var r0 []*apitypes.EventStream var r1 *ffapi.FilterResult var r2 error @@ -128,6 +140,10 @@ func (_m *RichQuery) ListStreams(ctx context.Context, filter ffapi.AndFilter) ([ func (_m *RichQuery) ListTransactionConfirmations(ctx context.Context, txID string, filter ffapi.AndFilter) ([]*apitypes.ConfirmationRecord, *ffapi.FilterResult, error) { ret := _m.Called(ctx, txID, filter) + if len(ret) == 0 { + panic("no return value specified for ListTransactionConfirmations") + } + var r0 []*apitypes.ConfirmationRecord var r1 *ffapi.FilterResult var r2 error @@ -163,6 +179,10 @@ func (_m *RichQuery) ListTransactionConfirmations(ctx context.Context, txID stri func (_m *RichQuery) ListTransactionHistory(ctx context.Context, txID string, filter ffapi.AndFilter) ([]*apitypes.TXHistoryRecord, *ffapi.FilterResult, error) { ret := _m.Called(ctx, txID, filter) + if len(ret) == 0 { + panic("no return value specified for ListTransactionHistory") + } + var r0 []*apitypes.TXHistoryRecord var r1 *ffapi.FilterResult var r2 error @@ -198,6 +218,10 @@ func (_m *RichQuery) ListTransactionHistory(ctx context.Context, txID string, fi func (_m *RichQuery) ListTransactions(ctx context.Context, filter ffapi.AndFilter) ([]*apitypes.ManagedTX, *ffapi.FilterResult, error) { ret := _m.Called(ctx, filter) + if len(ret) == 0 { + panic("no return value specified for ListTransactions") + } + var r0 []*apitypes.ManagedTX var r1 *ffapi.FilterResult var r2 error @@ -233,6 +257,10 @@ func (_m *RichQuery) ListTransactions(ctx context.Context, filter ffapi.AndFilte func (_m *RichQuery) NewConfirmationFilter(ctx context.Context) ffapi.FilterBuilder { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for NewConfirmationFilter") + } + var r0 ffapi.FilterBuilder if rf, ok := ret.Get(0).(func(context.Context) ffapi.FilterBuilder); ok { r0 = rf(ctx) @@ -249,6 +277,10 @@ func (_m *RichQuery) NewConfirmationFilter(ctx context.Context) ffapi.FilterBuil func (_m *RichQuery) NewListenerFilter(ctx context.Context) ffapi.FilterBuilder { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for NewListenerFilter") + } + var r0 ffapi.FilterBuilder if rf, ok := ret.Get(0).(func(context.Context) ffapi.FilterBuilder); ok { r0 = rf(ctx) @@ -265,6 +297,10 @@ func (_m *RichQuery) NewListenerFilter(ctx context.Context) ffapi.FilterBuilder func (_m *RichQuery) NewStreamFilter(ctx context.Context) ffapi.FilterBuilder { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for NewStreamFilter") + } + var r0 ffapi.FilterBuilder if rf, ok := ret.Get(0).(func(context.Context) ffapi.FilterBuilder); ok { r0 = rf(ctx) @@ -281,6 +317,10 @@ func (_m *RichQuery) NewStreamFilter(ctx context.Context) ffapi.FilterBuilder { func (_m *RichQuery) NewTransactionFilter(ctx context.Context) ffapi.FilterBuilder { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for NewTransactionFilter") + } + var r0 ffapi.FilterBuilder if rf, ok := ret.Get(0).(func(context.Context) ffapi.FilterBuilder); ok { r0 = rf(ctx) @@ -297,6 +337,10 @@ func (_m *RichQuery) NewTransactionFilter(ctx context.Context) ffapi.FilterBuild func (_m *RichQuery) NewTxHistoryFilter(ctx context.Context) ffapi.FilterBuilder { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for NewTxHistoryFilter") + } + var r0 ffapi.FilterBuilder if rf, ok := ret.Get(0).(func(context.Context) ffapi.FilterBuilder); ok { r0 = rf(ctx) diff --git a/mocks/persistencemocks/transaction_persistence.go b/mocks/persistencemocks/transaction_persistence.go index 78d26833..3f65203e 100644 --- a/mocks/persistencemocks/transaction_persistence.go +++ b/mocks/persistencemocks/transaction_persistence.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package persistencemocks @@ -32,6 +32,10 @@ func (_m *TransactionPersistence) AddTransactionConfirmations(ctx context.Contex _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for AddTransactionConfirmations") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, bool, ...*apitypes.Confirmation) error); ok { r0 = rf(ctx, txID, clearExisting, confirmations...) @@ -46,6 +50,10 @@ func (_m *TransactionPersistence) AddTransactionConfirmations(ctx context.Contex func (_m *TransactionPersistence) DeleteTransaction(ctx context.Context, txID string) error { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for DeleteTransaction") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, txID) @@ -60,6 +68,10 @@ func (_m *TransactionPersistence) DeleteTransaction(ctx context.Context, txID st func (_m *TransactionPersistence) GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for GetTransactionByID") + } + var r0 *apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*apitypes.ManagedTX, error)); ok { @@ -86,6 +98,10 @@ func (_m *TransactionPersistence) GetTransactionByID(ctx context.Context, txID s func (_m *TransactionPersistence) GetTransactionByIDWithStatus(ctx context.Context, txID string, history bool) (*apitypes.TXWithStatus, error) { ret := _m.Called(ctx, txID, history) + if len(ret) == 0 { + panic("no return value specified for GetTransactionByIDWithStatus") + } + var r0 *apitypes.TXWithStatus var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, bool) (*apitypes.TXWithStatus, error)); ok { @@ -112,6 +128,10 @@ func (_m *TransactionPersistence) GetTransactionByIDWithStatus(ctx context.Conte func (_m *TransactionPersistence) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, signer, nonce) + if len(ret) == 0 { + panic("no return value specified for GetTransactionByNonce") + } + var r0 *apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt) (*apitypes.ManagedTX, error)); ok { @@ -138,6 +158,10 @@ func (_m *TransactionPersistence) GetTransactionByNonce(ctx context.Context, sig func (_m *TransactionPersistence) GetTransactionConfirmations(ctx context.Context, txID string) ([]*apitypes.Confirmation, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for GetTransactionConfirmations") + } + var r0 []*apitypes.Confirmation var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) ([]*apitypes.Confirmation, error)); ok { @@ -164,6 +188,10 @@ func (_m *TransactionPersistence) GetTransactionConfirmations(ctx context.Contex func (_m *TransactionPersistence) GetTransactionReceipt(ctx context.Context, txID string) (*ffcapi.TransactionReceiptResponse, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for GetTransactionReceipt") + } + var r0 *ffcapi.TransactionReceiptResponse var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*ffcapi.TransactionReceiptResponse, error)); ok { @@ -190,6 +218,10 @@ func (_m *TransactionPersistence) GetTransactionReceipt(ctx context.Context, txI func (_m *TransactionPersistence) InsertTransactionPreAssignedNonce(ctx context.Context, tx *apitypes.ManagedTX) error { ret := _m.Called(ctx, tx) + if len(ret) == 0 { + panic("no return value specified for InsertTransactionPreAssignedNonce") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX) error); ok { r0 = rf(ctx, tx) @@ -204,6 +236,10 @@ func (_m *TransactionPersistence) InsertTransactionPreAssignedNonce(ctx context. func (_m *TransactionPersistence) InsertTransactionWithNextNonce(ctx context.Context, tx *apitypes.ManagedTX, lookupNextNonce txhandler.NextNonceCallback) error { ret := _m.Called(ctx, tx, lookupNextNonce) + if len(ret) == 0 { + panic("no return value specified for InsertTransactionWithNextNonce") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, txhandler.NextNonceCallback) error); ok { r0 = rf(ctx, tx, lookupNextNonce) @@ -218,6 +254,10 @@ func (_m *TransactionPersistence) InsertTransactionWithNextNonce(ctx context.Con func (_m *TransactionPersistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir txhandler.SortDirection) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, after, limit, dir) + if len(ret) == 0 { + panic("no return value specified for ListTransactionsByCreateTime") + } + var r0 []*apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, int, txhandler.SortDirection) ([]*apitypes.ManagedTX, error)); ok { @@ -244,6 +284,10 @@ func (_m *TransactionPersistence) ListTransactionsByCreateTime(ctx context.Conte func (_m *TransactionPersistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int, dir txhandler.SortDirection) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, signer, after, limit, dir) + if len(ret) == 0 { + panic("no return value specified for ListTransactionsByNonce") + } + var r0 []*apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt, int, txhandler.SortDirection) ([]*apitypes.ManagedTX, error)); ok { @@ -270,6 +314,10 @@ func (_m *TransactionPersistence) ListTransactionsByNonce(ctx context.Context, s func (_m *TransactionPersistence) ListTransactionsPending(ctx context.Context, afterSequenceID string, limit int, dir txhandler.SortDirection) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, afterSequenceID, limit, dir) + if len(ret) == 0 { + panic("no return value specified for ListTransactionsPending") + } + var r0 []*apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, int, txhandler.SortDirection) ([]*apitypes.ManagedTX, error)); ok { @@ -296,6 +344,10 @@ func (_m *TransactionPersistence) ListTransactionsPending(ctx context.Context, a func (_m *TransactionPersistence) SetTransactionReceipt(ctx context.Context, txID string, receipt *ffcapi.TransactionReceiptResponse) error { ret := _m.Called(ctx, txID, receipt) + if len(ret) == 0 { + panic("no return value specified for SetTransactionReceipt") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.TransactionReceiptResponse) error); ok { r0 = rf(ctx, txID, receipt) @@ -310,6 +362,10 @@ func (_m *TransactionPersistence) SetTransactionReceipt(ctx context.Context, txI func (_m *TransactionPersistence) UpdateTransaction(ctx context.Context, txID string, updates *apitypes.TXUpdates) error { ret := _m.Called(ctx, txID, updates) + if len(ret) == 0 { + panic("no return value specified for UpdateTransaction") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, *apitypes.TXUpdates) error); ok { r0 = rf(ctx, txID, updates) diff --git a/mocks/txhandlermocks/managed_tx_event_handler.go b/mocks/txhandlermocks/managed_tx_event_handler.go index 2b2d484e..2074fcbc 100644 --- a/mocks/txhandlermocks/managed_tx_event_handler.go +++ b/mocks/txhandlermocks/managed_tx_event_handler.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package txhandlermocks @@ -19,6 +19,10 @@ type ManagedTxEventHandler struct { func (_m *ManagedTxEventHandler) HandleEvent(ctx context.Context, e apitypes.ManagedTransactionEvent) error { ret := _m.Called(ctx, e) + if len(ret) == 0 { + panic("no return value specified for HandleEvent") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, apitypes.ManagedTransactionEvent) error); ok { r0 = rf(ctx, e) diff --git a/mocks/txhandlermocks/transaction_handler.go b/mocks/txhandlermocks/transaction_handler.go index 81993b55..e7aabd69 100644 --- a/mocks/txhandlermocks/transaction_handler.go +++ b/mocks/txhandlermocks/transaction_handler.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package txhandlermocks @@ -23,6 +23,10 @@ type TransactionHandler struct { func (_m *TransactionHandler) HandleCancelTransaction(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for HandleCancelTransaction") + } + var r0 *apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*apitypes.ManagedTX, error)); ok { @@ -49,6 +53,10 @@ func (_m *TransactionHandler) HandleCancelTransaction(ctx context.Context, txID func (_m *TransactionHandler) HandleNewContractDeployment(ctx context.Context, txReq *apitypes.ContractDeployRequest) (*apitypes.ManagedTX, bool, error) { ret := _m.Called(ctx, txReq) + if len(ret) == 0 { + panic("no return value specified for HandleNewContractDeployment") + } + var r0 *apitypes.ManagedTX var r1 bool var r2 error @@ -82,6 +90,10 @@ func (_m *TransactionHandler) HandleNewContractDeployment(ctx context.Context, t func (_m *TransactionHandler) HandleNewTransaction(ctx context.Context, txReq *apitypes.TransactionRequest) (*apitypes.ManagedTX, bool, error) { ret := _m.Called(ctx, txReq) + if len(ret) == 0 { + panic("no return value specified for HandleNewTransaction") + } + var r0 *apitypes.ManagedTX var r1 bool var r2 error @@ -115,6 +127,10 @@ func (_m *TransactionHandler) HandleNewTransaction(ctx context.Context, txReq *a func (_m *TransactionHandler) HandleResumeTransaction(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for HandleResumeTransaction") + } + var r0 *apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*apitypes.ManagedTX, error)); ok { @@ -141,6 +157,10 @@ func (_m *TransactionHandler) HandleResumeTransaction(ctx context.Context, txID func (_m *TransactionHandler) HandleSuspendTransaction(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, txID) + if len(ret) == 0 { + panic("no return value specified for HandleSuspendTransaction") + } + var r0 *apitypes.ManagedTX var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*apitypes.ManagedTX, error)); ok { @@ -167,6 +187,10 @@ func (_m *TransactionHandler) HandleSuspendTransaction(ctx context.Context, txID func (_m *TransactionHandler) HandleTransactionConfirmations(ctx context.Context, txID string, notification *apitypes.ConfirmationsNotification) error { ret := _m.Called(ctx, txID, notification) + if len(ret) == 0 { + panic("no return value specified for HandleTransactionConfirmations") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, *apitypes.ConfirmationsNotification) error); ok { r0 = rf(ctx, txID, notification) @@ -181,6 +205,10 @@ func (_m *TransactionHandler) HandleTransactionConfirmations(ctx context.Context func (_m *TransactionHandler) HandleTransactionReceiptReceived(ctx context.Context, txID string, receipt *ffcapi.TransactionReceiptResponse) error { ret := _m.Called(ctx, txID, receipt) + if len(ret) == 0 { + panic("no return value specified for HandleTransactionReceiptReceived") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.TransactionReceiptResponse) error); ok { r0 = rf(ctx, txID, receipt) @@ -200,6 +228,10 @@ func (_m *TransactionHandler) Init(ctx context.Context, toolkit *txhandler.Toolk func (_m *TransactionHandler) Start(ctx context.Context) (<-chan struct{}, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Start") + } + var r0 <-chan struct{} var r1 error if rf, ok := ret.Get(0).(func(context.Context) (<-chan struct{}, error)); ok { diff --git a/mocks/wsmocks/web_socket_channels.go b/mocks/wsmocks/web_socket_channels.go index 3f7135ff..ff0c66c9 100644 --- a/mocks/wsmocks/web_socket_channels.go +++ b/mocks/wsmocks/web_socket_channels.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package wsmocks @@ -16,6 +16,10 @@ type WebSocketChannels struct { func (_m *WebSocketChannels) GetChannels(topic string) (chan<- interface{}, chan<- interface{}, <-chan *ws.WebSocketCommandMessageOrError) { ret := _m.Called(topic) + if len(ret) == 0 { + panic("no return value specified for GetChannels") + } + var r0 chan<- interface{} var r1 chan<- interface{} var r2 <-chan *ws.WebSocketCommandMessageOrError diff --git a/mocks/wsmocks/web_socket_server.go b/mocks/wsmocks/web_socket_server.go index 7dba14d0..eca59b48 100644 --- a/mocks/wsmocks/web_socket_server.go +++ b/mocks/wsmocks/web_socket_server.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.4. DO NOT EDIT. +// Code generated by mockery v2.40.2. DO NOT EDIT. package wsmocks @@ -23,6 +23,10 @@ func (_m *WebSocketServer) Close() { func (_m *WebSocketServer) GetChannels(topic string) (chan<- interface{}, chan<- interface{}, <-chan *ws.WebSocketCommandMessageOrError) { ret := _m.Called(topic) + if len(ret) == 0 { + panic("no return value specified for GetChannels") + } + var r0 chan<- interface{} var r1 chan<- interface{} var r2 <-chan *ws.WebSocketCommandMessageOrError diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 1ba04978..d2f4b44a 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -199,10 +199,18 @@ func (lf ListenerFilters) Value() (driver.Value, error) { return jsonValue(lf) } +type ListenerType = fftypes.FFEnum + +var ( + ListenerTypeEvents = fftypes.FFEnumValue("fftm_listener_type", "events") + ListenerTypeBlocks = fftypes.FFEnumValue("fftm_listener_type", "blocks") +) + type Listener struct { ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` Created *fftypes.FFTime `ffstruct:"listener" json:"created"` Updated *fftypes.FFTime `ffstruct:"listener" json:"updated"` + Type *ListenerType `ffstruct:"listener" json:"type" ffenum:"fftm_listener_type"` Name *string `ffstruct:"listener" json:"name"` StreamID *fftypes.UUID `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` EthCompatAddress *string `ffstruct:"listener" json:"address,omitempty"` @@ -348,9 +356,10 @@ func CheckUpdateStringMap(changed bool, merged *map[string]string, old map[strin } type EventContext struct { - StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event - EthCompatSubID *fftypes.UUID `json:"subId"` // ID of the listener - EthCompat "subscription" naming - ListenerName string `json:"listenerName"` // name of the listener + StreamID *fftypes.UUID `json:"streamId,omitempty"` // the ID of the event stream for this event + EthCompatSubID *fftypes.UUID `json:"subId,omitempty"` // ID of the listener - EthCompat "subscription" naming + ListenerName string `json:"listenerName,omitempty"` // name of the listener + ListenerType ListenerType `json:"listenerType,omitempty"` } type EventBatch struct { @@ -364,17 +373,22 @@ type EventBatch struct { // The `data` is kept separate type EventWithContext struct { StandardContext EventContext - ffcapi.Event + Event *ffcapi.Event + BlockEvent *ffcapi.BlockEvent } func (e *EventWithContext) MarshalJSON() ([]byte, error) { m := make(map[string]interface{}) - if e.Info != nil { - jsonmap.AddJSONFieldsToMap(reflect.ValueOf(e.Info), m) + if e.Event != nil { + if e.Event.Info != nil { + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(e.Event.Info), m) + } + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(&e.Event.ID), m) + m["data"] = e.Event.Data + } else if e.BlockEvent != nil { + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(&e.BlockEvent), m) } - jsonmap.AddJSONFieldsToMap(reflect.ValueOf(&e.ID), m) jsonmap.AddJSONFieldsToMap(reflect.ValueOf(&e.StandardContext), m) - m["data"] = e.Data return json.Marshal(m) } @@ -383,14 +397,19 @@ func (e *EventWithContext) UnmarshalJSON(b []byte) error { var m fftypes.JSONObject err := json.Unmarshal(b, &m) if err == nil && m != nil { - e.Info = m - data := m["data"] - delete(m, "data") - if data != nil { - b, _ := json.Marshal(&data) - e.Data = fftypes.JSONAnyPtrBytes(b) + if m.GetString("listenerType") == string(ListenerTypeBlocks) { + e.BlockEvent = &ffcapi.BlockEvent{} + err = json.Unmarshal(b, &e.BlockEvent) + } else { + e.Event = &ffcapi.Event{Info: m} + data := m["data"] + delete(m, "data") + if data != nil { + b, _ := json.Marshal(&data) + e.Event.Data = fftypes.JSONAnyPtrBytes(b) + } + err = json.Unmarshal(b, &e.Event.ID) } - err = json.Unmarshal(b, &e.ID) if err == nil { err = json.Unmarshal(b, &e.StandardContext) } diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go index d6001df2..148aceb2 100644 --- a/pkg/apitypes/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -189,8 +189,9 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { StreamID: NewULID(), ListenerName: "listener1", EthCompatSubID: NewULID(), + ListenerType: ListenerTypeEvents, }, - Event: ffcapi.Event{ + Event: &ffcapi.Event{ ID: ffcapi.EventID{ ListenerID: fftypes.NewUUID(), BlockHash: "0x12345", @@ -214,8 +215,9 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { "blockNumber":"12345", "data": {"dk1":"dv1"}, "key1":"val1", - "listenerId":"`+e.ID.ListenerID.String()+`", + "listenerId":"`+e.Event.ID.ListenerID.String()+`", "listenerName":"listener1", + "listenerType": "events", "logIndex":"1", "signature":"ev()", "subId":"`+e.StandardContext.EthCompatSubID.String()+`", @@ -228,10 +230,57 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { err = json.Unmarshal(b, &e2) assert.NoError(t, err) - assert.Equal(t, e.ID.ListenerID, e2.ID.ListenerID) + assert.Equal(t, e.Event.ID.ListenerID, e2.Event.ID.ListenerID) + assert.Equal(t, e.StandardContext.StreamID, e2.StandardContext.StreamID) + assert.Equal(t, e.Event.Data, e2.Event.Data) + assert.Equal(t, "val1", e2.Event.Info.(fftypes.JSONObject).GetString("key1")) + +} + +func TestMarshalUnmarshalBlockEventOK(t *testing.T) { + + type customInfo struct { + InfoKey1 string `json:"key1"` + } + + e := &EventWithContext{ + StandardContext: EventContext{ + StreamID: NewULID(), + ListenerName: "listener1", + EthCompatSubID: NewULID(), + ListenerType: ListenerTypeBlocks, + }, + BlockEvent: &ffcapi.BlockEvent{ + ListenerID: fftypes.NewUUID(), + BlockInfo: ffcapi.BlockInfo{ + BlockHash: "0x12345", + BlockNumber: fftypes.NewFFBigInt(12345), + ParentHash: "0x23456", + TransactionHashes: []string{}, + }, + }, + } + + b, err := json.Marshal(&e) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "blockHash":"0x12345", + "parentHash": "0x23456", + "blockNumber":"12345", + "listenerId":"`+e.BlockEvent.ListenerID.String()+`", + "listenerName":"listener1", + "listenerType": "blocks", + "transactionHashes": [], + "subId":"`+e.StandardContext.EthCompatSubID.String()+`", + "streamId":"`+e.StandardContext.StreamID.String()+`" + }`, string(b)) + + var e2 *EventWithContext + err = json.Unmarshal(b, &e2) + assert.NoError(t, err) + + assert.Equal(t, e.BlockEvent.ListenerID, e2.BlockEvent.ListenerID) assert.Equal(t, e.StandardContext.StreamID, e2.StandardContext.StreamID) - assert.Equal(t, e.Data, e2.Data) - assert.Equal(t, "val1", e2.Info.(fftypes.JSONObject).GetString("key1")) } diff --git a/pkg/apitypes/base_request.go b/pkg/apitypes/base_request.go index 20c0f5d4..d2d55fa1 100644 --- a/pkg/apitypes/base_request.go +++ b/pkg/apitypes/base_request.go @@ -45,7 +45,8 @@ type RequestHeaders struct { type RequestType string const ( - RequestTypeSendTransaction RequestType = "SendTransaction" - RequestTypeQuery RequestType = "Query" - RequestTypeDeploy RequestType = "DeployContract" + RequestTypeSendTransaction RequestType = "SendTransaction" + RequestTypeQuery RequestType = "Query" + RequestTypeDeploy RequestType = "DeployContract" + RequestTypeTransactionReceipt RequestType = "TransactionReceipt" ) diff --git a/pkg/apitypes/txreceipt_request.go b/pkg/apitypes/txreceipt_request.go new file mode 100644 index 00000000..9876b4ac --- /dev/null +++ b/pkg/apitypes/txreceipt_request.go @@ -0,0 +1,33 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import ( + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +// TransactionReceiptRequest is the request payload to query for a receipt +type TransactionReceiptRequest struct { + Headers RequestHeaders `json:"headers"` + ffcapi.TransactionReceiptRequest +} + +// TransactionReceiptResponse is the response payload for a query +type TransactionReceiptResponse struct { + ffcapi.TransactionReceiptResponseBase + Events []*EventWithContext `json:"events,omitempty"` // this is the serialization format for events (historical complexity) +} diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 9f6bf61d..7b7c1489 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -100,14 +100,14 @@ type BlockHashEvent struct { // EventID are the set of required fields an FFCAPI compatible connector needs to map to the underlying blockchain constructs, to uniquely identify an event type EventID struct { - ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event - Signature string `json:"signature"` // The signature of this specific event (noting a listener might filter on multiple events) - BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes - BlockNumber fftypes.FFuint64 `json:"blockNumber"` // A numeric identifier for the block - TransactionHash string `json:"transactionHash"` // The transaction - TransactionIndex fftypes.FFuint64 `json:"transactionIndex"` // Index within the block of the transaction that emitted the event - LogIndex fftypes.FFuint64 `json:"logIndex"` // Index within the transaction of this emitted event log - Timestamp *fftypes.FFTime `json:"timestamp,omitempty"` // The on-chain timestamp + ListenerID *fftypes.UUID `json:"listenerId,omitempty"` // The listener for the event (omitted when decoding receipts) + Signature string `json:"signature"` // The signature of this specific event (noting a listener might filter on multiple events) + BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes + BlockNumber fftypes.FFuint64 `json:"blockNumber"` // A numeric identifier for the block + TransactionHash string `json:"transactionHash"` // The transaction + TransactionIndex fftypes.FFuint64 `json:"transactionIndex"` // Index within the block of the transaction that emitted the event + LogIndex fftypes.FFuint64 `json:"logIndex"` // Index within the transaction of this emitted event log + Timestamp *fftypes.FFTime `json:"timestamp,omitempty"` // The on-chain timestamp } // Event is a blockchain event that matches one of the started listeners, @@ -120,16 +120,36 @@ type Event struct { Data *fftypes.JSONAny // data } +type BlockEvent struct { + ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event + BlockInfo +} + func (e *Event) String() string { return e.ID.String() } +func (e *BlockEvent) String() string { + return fmt.Sprintf("block[%d/%s]", e.BlockNumber.Uint64(), e.BlockHash) +} + // EventListenerCheckpoint is the interface that a checkpoint must implement, basically to make it sortable. // The checkpoint must also be JSON serializable type EventListenerCheckpoint interface { LessThan(b EventListenerCheckpoint) bool } +// BlockListenerCheckpoint is an implementation of EventListenerCheckpoint for block listener, which are a special +// type of listener handled by the FFTM framework in the confirmation manager. +type BlockListenerCheckpoint struct { + Block uint64 `json:"block"` +} + +func (cp *BlockListenerCheckpoint) LessThan(b EventListenerCheckpoint) bool { + bcp := b.(*BlockListenerCheckpoint) + return cp.Block < bcp.Block +} + // String is unique in all cases for an event, by combining the protocol ID with the listener ID and block hash func (eid *EventID) String() string { return fmt.Sprintf("%s/B=%s/L=%s", eid.ProtocolID(), eid.BlockHash, eid.ListenerID) @@ -166,6 +186,7 @@ func evLess(eI *Event, eJ *Event) bool { type ListenerEvent struct { Checkpoint EventListenerCheckpoint `json:"checkpoint"` // the checkpoint information associated with the event, must be non-nil if the event is not removed Event *Event `json:"event"` // the event - for removed events, can only have the EventID fields set (to generate the protocol ID) + BlockEvent *BlockEvent `json:"blockEvent"` // the event for block listeners Removed bool `json:"removed,omitempty"` // when true, this is an explicit cancellation of a previous event } @@ -191,7 +212,7 @@ const ( // ErrorKnownTransaction if the exact transaction is already known ErrorKnownTransaction ErrorReason = "known_transaction" // ErrorReasonDownstreamDown if the downstream JSONRPC endpoint is down - ErrorReasonDownstreamDown = "downstream_down" + ErrorReasonDownstreamDown ErrorReason = "downstream_down" ) // TransactionInput is a standardized set of parameters that describe a transaction submission to a blockchain. diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go index 2cf29d64..de06f810 100644 --- a/pkg/ffcapi/api_test.go +++ b/pkg/ffcapi/api_test.go @@ -55,3 +55,21 @@ func TestSortEvents(t *testing.T) { assert.LessOrEqual(t, strings.Compare(listenerUpdates[i-1].Event.ID.ProtocolID(), listenerUpdates[i].Event.ID.ProtocolID()), 0) } } + +func TestSortBlockEventsString(t *testing.T) { + + assert.Equal(t, "block[12345/0x9614ad189f45ecff5f4949b22891c6bca7d83b40b50d8104bed101bc94395257]", (&BlockEvent{BlockInfo: BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(12345), + BlockHash: "0x9614ad189f45ecff5f4949b22891c6bca7d83b40b50d8104bed101bc94395257", + }}).String()) +} + +func TestBlockListenerCheckpoint(t *testing.T) { + + b10 := &BlockListenerCheckpoint{Block: 10} + b20 := &BlockListenerCheckpoint{Block: 20} + b30 := &BlockListenerCheckpoint{Block: 30} + assert.True(t, b10.LessThan(b20)) + assert.False(t, b30.LessThan(b20)) + assert.False(t, b20.LessThan(b20)) +} diff --git a/pkg/ffcapi/block_info_by_number.go b/pkg/ffcapi/block_info_by_number.go index d9c26112..aa3aaaae 100644 --- a/pkg/ffcapi/block_info_by_number.go +++ b/pkg/ffcapi/block_info_by_number.go @@ -22,6 +22,7 @@ import ( type BlockInfoByNumberRequest struct { BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + AllowCache bool `json:"allowCache"` ExpectedParentHash string `json:"expectedParentHash"` // If set then a mismatched parent hash should be considered a cache miss (if the connector does caching) } diff --git a/pkg/ffcapi/transaction_receipt.go b/pkg/ffcapi/transaction_receipt.go index 2decedac..0bd5f6b2 100644 --- a/pkg/ffcapi/transaction_receipt.go +++ b/pkg/ffcapi/transaction_receipt.go @@ -21,15 +21,25 @@ import ( ) type TransactionReceiptRequest struct { - TransactionHash string `json:"transactionHash"` + TransactionHash string `json:"transactionHash"` + IncludeLogs bool `json:"includeLogs"` + EventFilters []fftypes.JSONAny `json:"eventFilters"` + Methods []fftypes.JSONAny `json:"methods"` + ExtractSigner bool `json:"extractSigner"` } -type TransactionReceiptResponse struct { +type TransactionReceiptResponseBase struct { BlockNumber *fftypes.FFBigInt `json:"blockNumber"` TransactionIndex *fftypes.FFBigInt `json:"transactionIndex"` BlockHash string `json:"blockHash"` Success bool `json:"success"` ProtocolID string `json:"protocolId"` - ExtraInfo *fftypes.JSONAny `json:"extraInfo"` - ContractLocation *fftypes.JSONAny `json:"contractLocation"` + ExtraInfo *fftypes.JSONAny `json:"extraInfo,omitempty"` + ContractLocation *fftypes.JSONAny `json:"contractLocation,omitempty"` + Logs []fftypes.JSONAny `json:"logs,omitempty"` // all raw un-decoded logs should be included if includeLogs=true +} + +type TransactionReceiptResponse struct { + TransactionReceiptResponseBase + Events []*Event `json:"events,omitempty"` // only for events that matched the filter, and were decoded } diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index ce374845..4cad9b58 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -17,6 +17,7 @@ package fftm import ( + "encoding/json" "fmt" "strings" "testing" @@ -405,3 +406,119 @@ func TestNotFound(t *testing.T) { assert.Equal(t, 404, res.StatusCode()) assert.Regexp(t, "FF00167", errRes.Error) } + +func TestTransactionReceiptOK(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + mca := m.connector.(*ffcapimocks.API) + mca.On("TransactionReceipt", mock.Anything, mock.MatchedBy(func(req *ffcapi.TransactionReceiptRequest) bool { + return req.TransactionHash == `0x12345` + })).Return(&ffcapi.TransactionReceiptResponse{ + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockHash: "0x111111", + BlockNumber: fftypes.NewFFBigInt(10000), + TransactionIndex: fftypes.NewFFBigInt(10), + ProtocolID: "111/222/333", + Success: true, + }, + Events: []*ffcapi.Event{ + { + ID: ffcapi.EventID{Signature: "MyEvent()"}, + }, + }, + }, ffcapi.ErrorReason(""), nil) + + var queryRes map[string]interface{} + res, err := resty.New().R(). + SetBody(&apitypes.TransactionReceiptRequest{ + Headers: apitypes.RequestHeaders{ + ID: fftypes.NewUUID().String(), + Type: apitypes.RequestTypeTransactionReceipt, + }, + TransactionReceiptRequest: ffcapi.TransactionReceiptRequest{ + TransactionHash: "0x12345", + }, + }). + SetResult(&queryRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 202, res.StatusCode()) + + d, _ := json.Marshal(queryRes) + assert.JSONEq(t, `{ + "blockHash": "0x111111", + "blockNumber": "10000", + "protocolId": "111/222/333", + "success": true, + "transactionIndex": "10", + "events": [ + { + "blockHash": "", + "blockNumber": "0", + "data": null, + "logIndex": "0", + "signature": "MyEvent()", + "transactionHash": "", + "transactionIndex": "0" + } + ] + }`, string(d)) + + mca.AssertExpectations(t) + +} + +func TestTransactionReceiptFail(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + mca := m.connector.(*ffcapimocks.API) + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + res, err := resty.New().R(). + SetBody(&apitypes.TransactionReceiptRequest{ + Headers: apitypes.RequestHeaders{ + ID: fftypes.NewUUID().String(), + Type: apitypes.RequestTypeTransactionReceipt, + }, + TransactionReceiptRequest: ffcapi.TransactionReceiptRequest{ + TransactionHash: "0x12345", + }, + }). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 500, res.StatusCode()) + + mca.AssertExpectations(t) + +} + +func TestTransactionReceiptBadRequest(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(`{ + "headers": { + "id": "`+fftypes.NewUUID().String()+`", + "type": "TransactionReceipt" + }, + "eventFilters": "not an array" + }`, + ). + SetHeader("content-type", "application/json"). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21022", errRes.Error) + +} diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go index 81af0eec..df94dd93 100644 --- a/pkg/fftm/route__root_command.go +++ b/pkg/fftm/route__root_command.go @@ -107,6 +107,26 @@ var postRootCommand = func(m *manager) *ffapi.Route { return nil, err } return res.Outputs, nil + case apitypes.RequestTypeTransactionReceipt: + var tReq apitypes.TransactionReceiptRequest + if err = baseReq.UnmarshalTo(&tReq); err != nil { + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) + } + res, _, err := m.connector.TransactionReceipt(r.Req.Context(), &tReq.TransactionReceiptRequest) + if err != nil { + return nil, err + } + apiRes := &apitypes.TransactionReceiptResponse{ + TransactionReceiptResponseBase: res.TransactionReceiptResponseBase, + } + // Ugly necessity to work around the complex serialization interface in EventWithContext without + // moving or duplicating the code + for _, e := range res.Events { + apiRes.Events = append(apiRes.Events, &apitypes.EventWithContext{ + Event: e, + }) + } + return apiRes, nil default: return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgUnsupportedRequestType, baseReq.Headers.Type) } diff --git a/pkg/fftm/route_get_transaction_receipt_test.go b/pkg/fftm/route_get_transaction_receipt_test.go index 81ed61e6..de75b87f 100644 --- a/pkg/fftm/route_get_transaction_receipt_test.go +++ b/pkg/fftm/route_get_transaction_receipt_test.go @@ -37,7 +37,9 @@ func TestGetTransactionReceiptOK(t *testing.T) { mpm := m.persistence.(*persistencemocks.Persistence) mpm.On("GetTransactionReceipt", mock.Anything, "tx1").Return( &ffcapi.TransactionReceiptResponse{ - BlockHash: "0x123456", + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockHash: "0x123456", + }, }, nil, ) diff --git a/pkg/fftm/transaction_events_handler_test.go b/pkg/fftm/transaction_events_handler_test.go index d0ea6bcc..44e91183 100644 --- a/pkg/fftm/transaction_events_handler_test.go +++ b/pkg/fftm/transaction_events_handler_test.go @@ -81,8 +81,10 @@ func TestHandleTransactionProcessFailEvent(t *testing.T) { TransactionHash: "0x1111", } receipt := &ffcapi.TransactionReceiptResponse{ - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), - ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + }, } eh := newTestManagedTransactionEventHandler() mws := &wsmocks.WebSocketServer{} diff --git a/pkg/txhandler/simple/policyloop_test.go b/pkg/txhandler/simple/policyloop_test.go index c7412bfa..66baa9ac 100644 --- a/pkg/txhandler/simple/policyloop_test.go +++ b/pkg/txhandler/simple/policyloop_test.go @@ -129,12 +129,14 @@ func TestPolicyLoopE2EOk(t *testing.T) { })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), - Success: true, - ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + Success: true, + ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + }, }) n.Transaction.Confirmations(context.Background(), &apitypes.ConfirmationsNotification{Confirmed: true}) }).Return(nil) @@ -199,12 +201,14 @@ func TestPolicyLoopIgnoreTransactionInformationalEventHandlingErrors(t *testing. sth.inflight = []*pendingState{} n := args[0].(*confirmations.Notification) n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), - Success: true, - ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + Success: true, + ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + }, }) n.Transaction.Confirmations(context.Background(), &apitypes.ConfirmationsNotification{Confirmed: true}) }).Return(nil) @@ -310,11 +314,13 @@ func TestPolicyLoopE2EReverted(t *testing.T) { })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), - Success: false, + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + Success: false, + }, }) n.Transaction.Confirmations(context.Background(), &apitypes.ConfirmationsNotification{Confirmed: true}) }).Return(nil) @@ -394,11 +400,13 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), - Success: true, + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + Success: true, + }, }) n.Transaction.Confirmations(context.Background(), &apitypes.ConfirmationsNotification{Confirmed: true}) }).Return(nil) @@ -574,12 +582,14 @@ func TestPolicyLoopUpdateFail(t *testing.T) { })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), - Success: true, - ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + Success: true, + ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + }, }) n.Transaction.Confirmations(context.Background(), &apitypes.ConfirmationsNotification{Confirmed: true}) }).Return(nil) @@ -649,12 +659,14 @@ func TestPolicyLoopUpdateEventHandlerError(t *testing.T) { })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), - Success: true, - ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + ProtocolID: fmt.Sprintf("%.12d/%.6d", fftypes.NewFFBigInt(12345).Int64(), fftypes.NewFFBigInt(10).Int64()), + Success: true, + ContractLocation: fftypes.JSONAnyPtr(`{"address": "0x24746b95d118b2b4e8d07b06b1bad988fbf9415d"}`), + }, }) n.Transaction.Confirmations(context.Background(), &apitypes.ConfirmationsNotification{Confirmed: true}) }).Return(nil) diff --git a/pkg/txhandler/simple/simple_transaction_hander_test.go b/pkg/txhandler/simple/simple_transaction_hander_test.go index 70534ce9..619533d6 100644 --- a/pkg/txhandler/simple/simple_transaction_hander_test.go +++ b/pkg/txhandler/simple/simple_transaction_hander_test.go @@ -99,12 +99,12 @@ func newTestTransactionHandlerFactoryWithFilePersistence(t *testing.T) (*Transac mockFFCAPI := &ffcapimocks.API{} return f, &txhandler.Toolkit{ - Connector: mockFFCAPI, - TXHistory: filePersistence, - TXPersistence: filePersistence, - MetricsManager: metrics.NewMetricsManager(context.Background()), - EventHandler: mockEventHandler, - }, mockFFCAPI, conf + Connector: mockFFCAPI, + TXHistory: filePersistence, + TXPersistence: filePersistence, + MetricsManager: metrics.NewMetricsManager(context.Background()), + EventHandler: mockEventHandler, + }, mockFFCAPI, conf } func newTestTransactionHandler(t *testing.T) txhandler.TransactionHandler { @@ -752,7 +752,9 @@ func TestNoOpWithReceipt(t *testing.T) { FirstSubmit: submitTime, } receipt := &ffcapi.TransactionReceiptResponse{ - BlockHash: "0x39e2664effa5ad0651c35f1fe3b4c4b90492b1955fee731c2e9fb4d6518de114", + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + BlockHash: "0x39e2664effa5ad0651c35f1fe3b4c4b90492b1955fee731c2e9fb4d6518de114", + }, } sth := th.(*simpleTransactionHandler)