From a2702970c53829170901d96c50cf96b6f91d17d8 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 10 Feb 2022 13:24:32 -0500 Subject: [PATCH 01/10] Add Operations Manager and use for token operations Signed-off-by: Andrew Richardson --- Makefile | 3 +- internal/assets/manager.go | 68 ++--- internal/assets/manager_test.go | 17 +- internal/assets/token_pool.go | 26 +- internal/assets/token_pool_test.go | 205 +++++++++----- internal/assets/token_transfer.go | 21 +- internal/assets/token_transfer_test.go | 263 ++++++++---------- internal/config/config.go | 9 - .../definition_handler_tokenpool.go | 6 +- .../definition_handler_tokenpool_test.go | 10 +- internal/events/operation_update.go | 11 +- internal/events/token_pool_created.go | 7 +- internal/events/token_pool_created_test.go | 8 +- internal/events/tokens_transferred.go | 4 +- internal/i18n/en_translations.go | 1 + internal/operations/manager.go | 151 ++++++++++ internal/operations/manager_test.go | 48 ++++ internal/orchestrator/orchestrator.go | 10 +- internal/orchestrator/orchestrator_test.go | 12 + internal/syncasync/sync_async_bridge.go | 4 +- internal/tokens/fftokens/fftokens.go | 4 +- internal/tokens/fftokens/fftokens_test.go | 18 +- internal/txcommon/token_inputs.go | 65 +++-- internal/txcommon/token_inputs_test.go | 55 ++-- mocks/assetmocks/manager.go | 29 +- mocks/operationmocks/manager.go | 29 ++ mocks/tokenmocks/plugin.go | 14 +- pkg/tokens/plugin.go | 2 +- 28 files changed, 659 insertions(+), 441 deletions(-) create mode 100644 internal/operations/manager.go create mode 100644 internal/operations/manager_test.go create mode 100644 mocks/operationmocks/manager.go diff --git a/Makefile b/Makefile index 08e436d773..05df9b87a6 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,7 @@ $(eval $(call makemock, internal/orchestrator, Orchestrator, orchestra $(eval $(call makemock, internal/apiserver, Server, apiservermocks)) $(eval $(call makemock, internal/apiserver, IServer, apiservermocks)) $(eval $(call makemock, internal/metrics, Manager, metricsmocks)) +$(eval $(call makemock, internal/operations, Manager, operationmocks)) firefly-nocgo: ${GOFILES} CGO_ENABLED=0 $(VGO) build -o ${BINARY_NAME}-nocgo -ldflags "-X main.buildDate=`date -u +\"%Y-%m-%dT%H:%M:%SZ\"` -X main.buildVersion=$(BUILD_VERSION)" -tags=prod -tags=prod -v @@ -91,4 +92,4 @@ swagger: manifest: ./manifestgen.sh docker: - ./docker_build.sh $(DOCKER_ARGS) \ No newline at end of file + ./docker_build.sh $(DOCKER_ARGS) diff --git a/internal/assets/manager.go b/internal/assets/manager.go index 595684e3f3..cbf731ce45 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -20,13 +20,12 @@ import ( "context" "github.com/hyperledger/firefly/internal/broadcast" - "github.com/hyperledger/firefly/internal/config" "github.com/hyperledger/firefly/internal/data" "github.com/hyperledger/firefly/internal/i18n" "github.com/hyperledger/firefly/internal/identity" "github.com/hyperledger/firefly/internal/metrics" + "github.com/hyperledger/firefly/internal/operations" "github.com/hyperledger/firefly/internal/privatemessaging" - "github.com/hyperledger/firefly/internal/retry" "github.com/hyperledger/firefly/internal/syncasync" "github.com/hyperledger/firefly/internal/sysmessaging" "github.com/hyperledger/firefly/internal/txcommon" @@ -37,7 +36,7 @@ import ( type Manager interface { CreateTokenPool(ctx context.Context, ns string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) - ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, event *fftypes.BlockchainEvent) error + ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) error GetTokenPools(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) GetTokenPool(ctx context.Context, ns, connector, poolName string) (*fftypes.TokenPool, error) GetTokenPoolByNameOrID(ctx context.Context, ns string, poolNameOrID string) (*fftypes.TokenPool, error) @@ -55,45 +54,38 @@ type Manager interface { TransferTokens(ctx context.Context, ns string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (*fftypes.TokenTransfer, error) GetTokenConnectors(ctx context.Context, ns string) ([]*fftypes.TokenConnector, error) - - Start() error - WaitStop() } type assetManager struct { - ctx context.Context - database database.Plugin - txHelper txcommon.Helper - identity identity.Manager - data data.Manager - syncasync syncasync.Bridge - broadcast broadcast.Manager - messaging privatemessaging.Manager - tokens map[string]tokens.Plugin - retry retry.Retry - metrics metrics.Manager + ctx context.Context + database database.Plugin + txHelper txcommon.Helper + identity identity.Manager + data data.Manager + syncasync syncasync.Bridge + broadcast broadcast.Manager + messaging privatemessaging.Manager + tokens map[string]tokens.Plugin + metrics metrics.Manager + operations operations.Manager } -func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manager, dm data.Manager, sa syncasync.Bridge, bm broadcast.Manager, pm privatemessaging.Manager, ti map[string]tokens.Plugin, mm metrics.Manager) (Manager, error) { - if di == nil || im == nil || sa == nil || bm == nil || pm == nil || ti == nil { +func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manager, dm data.Manager, sa syncasync.Bridge, bm broadcast.Manager, pm privatemessaging.Manager, ti map[string]tokens.Plugin, mm metrics.Manager, ops operations.Manager) (Manager, error) { + if di == nil || im == nil || sa == nil || bm == nil || pm == nil || ti == nil || mm == nil || ops == nil { return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) } am := &assetManager{ - ctx: ctx, - database: di, - txHelper: txcommon.NewTransactionHelper(di), - identity: im, - data: dm, - syncasync: sa, - broadcast: bm, - messaging: pm, - tokens: ti, - retry: retry.Retry{ - InitialDelay: config.GetDuration(config.AssetManagerRetryInitialDelay), - MaximumDelay: config.GetDuration(config.AssetManagerRetryMaxDelay), - Factor: config.GetFloat64(config.AssetManagerRetryFactor), - }, - metrics: mm, + ctx: ctx, + database: di, + txHelper: txcommon.NewTransactionHelper(di), + identity: im, + data: dm, + syncasync: sa, + broadcast: bm, + messaging: pm, + tokens: ti, + metrics: mm, + operations: ops, } return am, nil } @@ -141,14 +133,6 @@ func (am *assetManager) GetTokenConnectors(ctx context.Context, ns string) ([]*f return connectors, nil } -func (am *assetManager) Start() error { - return nil -} - -func (am *assetManager) WaitStop() { - // No go routines -} - func (am *assetManager) getTokenConnectorName(ctx context.Context, ns string) (string, error) { tokenConnectors, err := am.GetTokenConnectors(ctx, ns) if err != nil { diff --git a/internal/assets/manager_test.go b/internal/assets/manager_test.go index a510e35071..92fc02451a 100644 --- a/internal/assets/manager_test.go +++ b/internal/assets/manager_test.go @@ -24,6 +24,7 @@ import ( "github.com/hyperledger/firefly/mocks/datamocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/mocks/metricsmocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/mocks/privatemessagingmocks" "github.com/hyperledger/firefly/mocks/syncasyncmocks" "github.com/hyperledger/firefly/mocks/tokenmocks" @@ -45,10 +46,11 @@ func newTestAssets(t *testing.T) (*assetManager, func()) { mpm := &privatemessagingmocks.Manager{} mti := &tokenmocks.Plugin{} mm := &metricsmocks.Manager{} + mom := &operationmocks.Manager{} mti.On("Name").Return("ut_tokens").Maybe() mm.On("IsMetricsEnabled").Return(false) ctx, cancel := context.WithCancel(context.Background()) - a, err := NewAssetManager(ctx, mdi, mim, mdm, msa, mbm, mpm, map[string]tokens.Plugin{"magic-tokens": mti}, mm) + a, err := NewAssetManager(ctx, mdi, mim, mdm, msa, mbm, mpm, map[string]tokens.Plugin{"magic-tokens": mti}, mm, mom) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { rag.ReturnArguments = mock.Arguments{a[1].(func(context.Context) error)(a[0].(context.Context))} @@ -69,11 +71,12 @@ func newTestAssetsWithMetrics(t *testing.T) (*assetManager, func()) { mpm := &privatemessagingmocks.Manager{} mti := &tokenmocks.Plugin{} mm := &metricsmocks.Manager{} + mom := &operationmocks.Manager{} mti.On("Name").Return("ut_tokens").Maybe() mm.On("IsMetricsEnabled").Return(true) mm.On("TransferSubmitted", mock.Anything) ctx, cancel := context.WithCancel(context.Background()) - a, err := NewAssetManager(ctx, mdi, mim, mdm, msa, mbm, mpm, map[string]tokens.Plugin{"magic-tokens": mti}, mm) + a, err := NewAssetManager(ctx, mdi, mim, mdm, msa, mbm, mpm, map[string]tokens.Plugin{"magic-tokens": mti}, mm, mom) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { rag.ReturnArguments = mock.Arguments{a[1].(func(context.Context) error)(a[0].(context.Context))} @@ -85,18 +88,10 @@ func newTestAssetsWithMetrics(t *testing.T) (*assetManager, func()) { } func TestInitFail(t *testing.T) { - _, err := NewAssetManager(context.Background(), nil, nil, nil, nil, nil, nil, nil, nil) + _, err := NewAssetManager(context.Background(), nil, nil, nil, nil, nil, nil, nil, nil, nil) assert.Regexp(t, "FF10128", err) } -func TestStartStop(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - am.Start() - am.WaitStop() -} - func TestGetTokenBalances(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() diff --git a/internal/assets/token_pool.go b/internal/assets/token_pool.go index fe26dd0b9f..c65964c4f7 100644 --- a/internal/assets/token_pool.go +++ b/internal/assets/token_pool.go @@ -81,24 +81,19 @@ func (am *assetManager) createTokenPoolInternal(ctx context.Context, pool *fftyp pool.Namespace, txid, fftypes.OpTypeTokenCreatePool) - txcommon.AddTokenPoolCreateInputs(op, pool) - - return am.database.InsertOperation(ctx, op) + if err = txcommon.AddTokenPoolCreateInputs(op, pool); err == nil { + err = am.database.InsertOperation(ctx, op) + } + return err }) if err != nil { return nil, err } - if complete, err := plugin.CreateTokenPool(ctx, op.ID, pool); err != nil { - am.txHelper.WriteOperationFailure(ctx, op.ID, err) - return nil, err - } else if complete { - am.txHelper.WriteOperationSuccess(ctx, op.ID, nil) - } - return pool, nil + return pool, am.operations.StartOperation(ctx, op) } -func (am *assetManager) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, event *fftypes.BlockchainEvent) error { +func (am *assetManager) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) error { plugin, err := am.selectTokenPlugin(ctx, pool.Connector) if err != nil { return err @@ -109,17 +104,12 @@ func (am *assetManager) ActivateTokenPool(ctx context.Context, pool *fftypes.Tok pool.Namespace, pool.TX.ID, fftypes.OpTypeTokenActivatePool) + txcommon.AddTokenPoolActivateInputs(op, pool.ID, blockchainInfo) if err := am.database.InsertOperation(ctx, op); err != nil { return err } - if complete, err := plugin.ActivateTokenPool(ctx, op.ID, pool, event); err != nil { - am.txHelper.WriteOperationFailure(ctx, op.ID, err) - return err - } else if complete { - am.txHelper.WriteOperationSuccess(ctx, op.ID, nil) - } - return nil + return am.operations.StartOperation(ctx, op) } func (am *assetManager) GetTokenPools(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) { diff --git a/internal/assets/token_pool_test.go b/internal/assets/token_pool_test.go index 4cc7fc2615..116d4b1d55 100644 --- a/internal/assets/token_pool_test.go +++ b/internal/assets/token_pool_test.go @@ -23,8 +23,8 @@ import ( "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/datamocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/mocks/syncasyncmocks" - "github.com/hyperledger/firefly/mocks/tokenmocks" "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" @@ -56,17 +56,25 @@ func TestCreateTokenPoolUnknownConnectorSuccess(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(false, nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + })).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestCreateTokenPoolUnknownConnectorNoConnectors(t *testing.T) { @@ -84,6 +92,8 @@ func TestCreateTokenPoolUnknownConnectorNoConnectors(t *testing.T) { _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.Regexp(t, "FF10292", err) + + mdm.AssertExpectations(t) } func TestCreateTokenPoolUnknownConnectorMultipleConnectors(t *testing.T) { @@ -102,6 +112,8 @@ func TestCreateTokenPoolUnknownConnectorMultipleConnectors(t *testing.T) { _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.Regexp(t, "FF10292", err) + + mdm.AssertExpectations(t) } func TestCreateTokenPoolMissingNamespace(t *testing.T) { @@ -112,27 +124,13 @@ func TestCreateTokenPoolMissingNamespace(t *testing.T) { Name: "testpool", } - mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(fmt.Errorf("pop")) - msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mth := am.txHelper.(*txcommonmocks.Helper) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil).Times(2) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything).Return(false, nil).Times(1) - mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) - mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil).Times(1) - msa.On("WaitForTokenPool", context.Background(), "ns1", mock.Anything, mock.Anything). - Run(func(args mock.Arguments) { - send := args[3].(syncasync.RequestSender) - send(context.Background()) - }). - Return(nil, nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.EqualError(t, err, "pop") + + mdm.AssertExpectations(t) } func TestCreateTokenPoolNoConnectors(t *testing.T) { @@ -149,6 +147,8 @@ func TestCreateTokenPoolNoConnectors(t *testing.T) { _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.Regexp(t, "FF10292", err) + + mdm.AssertExpectations(t) } func TestCreateTokenPoolIdentityFail(t *testing.T) { @@ -166,6 +166,9 @@ func TestCreateTokenPoolIdentityFail(t *testing.T) { _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.EqualError(t, err, "pop") + + mdm.AssertExpectations(t) + mim.AssertExpectations(t) } func TestCreateTokenPoolWrongConnector(t *testing.T) { @@ -177,18 +180,16 @@ func TestCreateTokenPoolWrongConnector(t *testing.T) { Name: "testpool", } - mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) mim := am.identity.(*identitymanagermocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) - mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.Regexp(t, "FF10272", err) + + mdm.AssertExpectations(t) + mim.AssertExpectations(t) } func TestCreateTokenPoolFail(t *testing.T) { @@ -202,18 +203,25 @@ func TestCreateTokenPoolFail(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(false, fmt.Errorf("pop")) - mth.On("WriteOperationFailure", context.Background(), mock.Anything, fmt.Errorf("pop")) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + })).Return(fmt.Errorf("pop")) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.Regexp(t, "pop", err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestCreateTokenPoolTransactionFail(t *testing.T) { @@ -234,6 +242,10 @@ func TestCreateTokenPoolTransactionFail(t *testing.T) { _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.Regexp(t, "pop", err) + + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) } func TestCreateTokenPoolOpInsertFail(t *testing.T) { @@ -256,6 +268,11 @@ func TestCreateTokenPoolOpInsertFail(t *testing.T) { _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.Regexp(t, "pop", err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) } func TestCreateTokenPoolSyncSuccess(t *testing.T) { @@ -269,18 +286,25 @@ func TestCreateTokenPoolSyncSuccess(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(true, nil) - mth.On("WriteOperationSuccess", context.Background(), mock.Anything, mock.Anything) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + })).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestCreateTokenPoolAsyncSuccess(t *testing.T) { @@ -294,17 +318,25 @@ func TestCreateTokenPoolAsyncSuccess(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(false, nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + })).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestCreateTokenPoolConfirm(t *testing.T) { @@ -319,23 +351,32 @@ func TestCreateTokenPoolConfirm(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil).Times(2) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything).Return(false, nil).Times(1) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) - mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil).Times(1) + mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) msa.On("WaitForTokenPool", context.Background(), "ns1", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { send := args[3].(syncasync.RequestSender) send(context.Background()) }). Return(nil, nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + })).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, true) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + msa.AssertExpectations(t) + mom.AssertExpectations(t) } func TestActivateTokenPool(t *testing.T) { @@ -343,22 +384,29 @@ func TestActivateTokenPool(t *testing.T) { defer cancel() pool := &fftypes.TokenPool{ + ID: fftypes.NewUUID(), Namespace: "ns1", Connector: "magic-tokens", } - ev := &fftypes.BlockchainEvent{} + info := fftypes.JSONObject{ + "some": "info", + } - mdm := am.data.(*datamocks.Manager) mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mom := am.operations.(*operationmocks.Manager) mdi.On("InsertOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeTokenActivatePool })).Return(nil) - mti.On("ActivateTokenPool", context.Background(), mock.Anything, pool, ev).Return(false, nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + assert.Equal(t, info, op.Input.GetObject("info")) + return op.Type == fftypes.OpTypeTokenActivatePool && op.Input.GetString("id") == pool.ID.String() + })).Return(nil) - err := am.ActivateTokenPool(context.Background(), pool, ev) + err := am.ActivateTokenPool(context.Background(), pool, info) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mom.AssertExpectations(t) } func TestActivateTokenPoolBadConnector(t *testing.T) { @@ -369,12 +417,9 @@ func TestActivateTokenPoolBadConnector(t *testing.T) { Namespace: "ns1", Connector: "bad", } - ev := &fftypes.BlockchainEvent{} - - mdm := am.data.(*datamocks.Manager) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + info := fftypes.JSONObject{} - err := am.ActivateTokenPool(context.Background(), pool, ev) + err := am.ActivateTokenPool(context.Background(), pool, info) assert.Regexp(t, "FF10272", err) } @@ -386,17 +431,17 @@ func TestActivateTokenPoolOpInsertFail(t *testing.T) { Namespace: "ns1", Connector: "magic-tokens", } - ev := &fftypes.BlockchainEvent{} + info := fftypes.JSONObject{} - mdm := am.data.(*datamocks.Manager) mdi := am.database.(*databasemocks.Plugin) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mdi.On("InsertOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeTokenActivatePool })).Return(fmt.Errorf("pop")) - err := am.ActivateTokenPool(context.Background(), pool, ev) + err := am.ActivateTokenPool(context.Background(), pool, info) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) } func TestActivateTokenPoolFail(t *testing.T) { @@ -407,21 +452,27 @@ func TestActivateTokenPoolFail(t *testing.T) { Namespace: "ns1", Connector: "magic-tokens", } - ev := &fftypes.BlockchainEvent{} + info := fftypes.JSONObject{ + "some": "info", + } - mdm := am.data.(*datamocks.Manager) mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mth := am.txHelper.(*txcommonmocks.Helper) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mom := am.operations.(*operationmocks.Manager) mdi.On("InsertOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeTokenActivatePool })).Return(nil) - mti.On("ActivateTokenPool", context.Background(), mock.Anything, pool, ev).Return(false, fmt.Errorf("pop")) - mth.On("WriteOperationFailure", context.Background(), mock.Anything, fmt.Errorf("pop")) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + assert.Equal(t, info, op.Input.GetObject("info")) + return op.Type == fftypes.OpTypeTokenActivatePool && op.Input.GetString("id") == pool.ID.String() + })).Return(fmt.Errorf("pop")) - err := am.ActivateTokenPool(context.Background(), pool, ev) + err := am.ActivateTokenPool(context.Background(), pool, info) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestActivateTokenPoolSyncSuccess(t *testing.T) { @@ -432,21 +483,27 @@ func TestActivateTokenPoolSyncSuccess(t *testing.T) { Namespace: "ns1", Connector: "magic-tokens", } - ev := &fftypes.BlockchainEvent{} + info := fftypes.JSONObject{ + "some": "info", + } - mdm := am.data.(*datamocks.Manager) mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mth := am.txHelper.(*txcommonmocks.Helper) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mom := am.operations.(*operationmocks.Manager) mdi.On("InsertOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeTokenActivatePool })).Return(nil) - mti.On("ActivateTokenPool", context.Background(), mock.Anything, pool, ev).Return(true, nil) - mth.On("WriteOperationSuccess", context.Background(), mock.Anything, mock.Anything) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + assert.Equal(t, info, op.Input.GetObject("info")) + return op.Type == fftypes.OpTypeTokenActivatePool && op.Input.GetString("id") == pool.ID.String() + })).Return(nil) - err := am.ActivateTokenPool(context.Background(), pool, ev) + err := am.ActivateTokenPool(context.Background(), pool, info) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestGetTokenPool(t *testing.T) { @@ -457,6 +514,8 @@ func TestGetTokenPool(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "abc").Return(&fftypes.TokenPool{}, nil) _, err := am.GetTokenPool(context.Background(), "ns1", "magic-tokens", "abc") assert.NoError(t, err) + + mdi.AssertExpectations(t) } func TestGetTokenPoolNotFound(t *testing.T) { @@ -467,6 +526,8 @@ func TestGetTokenPoolNotFound(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "abc").Return(nil, nil) _, err := am.GetTokenPool(context.Background(), "ns1", "magic-tokens", "abc") assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) } func TestGetTokenPoolFailed(t *testing.T) { @@ -477,6 +538,8 @@ func TestGetTokenPoolFailed(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "abc").Return(nil, fmt.Errorf("pop")) _, err := am.GetTokenPool(context.Background(), "ns1", "magic-tokens", "abc") assert.Regexp(t, "pop", err) + + mdi.AssertExpectations(t) } func TestGetTokenPoolBadPlugin(t *testing.T) { @@ -512,6 +575,8 @@ func TestGetTokenPoolByID(t *testing.T) { mdi.On("GetTokenPoolByID", context.Background(), u).Return(&fftypes.TokenPool{}, nil) _, err := am.GetTokenPoolByNameOrID(context.Background(), "ns1", u.String()) assert.NoError(t, err) + + mdi.AssertExpectations(t) } func TestGetTokenPoolByIDBadNamespace(t *testing.T) { @@ -531,6 +596,8 @@ func TestGetTokenPoolByIDBadID(t *testing.T) { mdi.On("GetTokenPoolByID", context.Background(), u).Return(nil, fmt.Errorf("pop")) _, err := am.GetTokenPoolByNameOrID(context.Background(), "ns1", u.String()) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) } func TestGetTokenPoolByIDNilPool(t *testing.T) { @@ -542,6 +609,8 @@ func TestGetTokenPoolByIDNilPool(t *testing.T) { mdi.On("GetTokenPoolByID", context.Background(), u).Return(nil, nil) _, err := am.GetTokenPoolByNameOrID(context.Background(), "ns1", u.String()) assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) } func TestGetTokenPoolByName(t *testing.T) { @@ -552,6 +621,8 @@ func TestGetTokenPoolByName(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "abc").Return(&fftypes.TokenPool{}, nil) _, err := am.GetTokenPoolByNameOrID(context.Background(), "ns1", "abc") assert.NoError(t, err) + + mdi.AssertExpectations(t) } func TestGetTokenPoolByNameBadName(t *testing.T) { @@ -570,6 +641,8 @@ func TestGetTokenPoolByNameNilPool(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "abc").Return(nil, fmt.Errorf("pop")) _, err := am.GetTokenPoolByNameOrID(context.Background(), "ns1", "abc") assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) } func TestGetTokenPools(t *testing.T) { @@ -583,6 +656,8 @@ func TestGetTokenPools(t *testing.T) { mdi.On("GetTokenPools", context.Background(), f).Return([]*fftypes.TokenPool{}, nil, nil) _, _, err := am.GetTokenPools(context.Background(), "ns1", f) assert.NoError(t, err) + + mdi.AssertExpectations(t) } func TestGetTokenPoolsBadNamespace(t *testing.T) { diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go index 75a21cca06..2f08e16c4d 100644 --- a/internal/assets/token_transfer.go +++ b/internal/assets/token_transfer.go @@ -18,7 +18,6 @@ package assets import ( "context" - "fmt" "github.com/hyperledger/firefly/internal/i18n" "github.com/hyperledger/firefly/internal/sysmessaging" @@ -228,10 +227,9 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er return nil } - var pool *fftypes.TokenPool var op *fftypes.Operation err = s.mgr.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { - pool, err = s.mgr.GetTokenPoolByNameOrID(ctx, s.namespace, s.transfer.Pool) + pool, err := s.mgr.GetTokenPoolByNameOrID(ctx, s.namespace, s.transfer.Pool) if err != nil { return err } @@ -246,6 +244,7 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er s.transfer.TX.ID = txid s.transfer.TX.Type = fftypes.TransactionTypeTokenTransfer + s.transfer.TokenTransfer.Pool = pool.ID op = fftypes.NewOperation( plugin, @@ -269,21 +268,7 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er return err } - switch s.transfer.Type { - case fftypes.TokenTransferTypeMint: - err = plugin.MintTokens(ctx, op.ID, pool.ProtocolID, &s.transfer.TokenTransfer) - case fftypes.TokenTransferTypeTransfer: - err = plugin.TransferTokens(ctx, op.ID, pool.ProtocolID, &s.transfer.TokenTransfer) - case fftypes.TokenTransferTypeBurn: - err = plugin.BurnTokens(ctx, op.ID, pool.ProtocolID, &s.transfer.TokenTransfer) - default: - panic(fmt.Sprintf("unknown transfer type: %v", s.transfer.Type)) - } - - if err != nil { - s.mgr.txHelper.WriteOperationFailure(ctx, op.ID, err) - } - return err + return s.mgr.operations.StartOperation(ctx, op) } func (s *transferSender) buildTransferMessage(ctx context.Context, ns string, in *fftypes.MessageInOut) (sysmessaging.MessageSender, error) { diff --git a/internal/assets/token_transfer_test.go b/internal/assets/token_transfer_test.go index cb54c3ded2..d3e091d4cc 100644 --- a/internal/assets/token_transfer_test.go +++ b/internal/assets/token_transfer_test.go @@ -24,10 +24,10 @@ import ( "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/datamocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/mocks/privatemessagingmocks" "github.com/hyperledger/firefly/mocks/syncasyncmocks" "github.com/hyperledger/firefly/mocks/sysmessagingmocks" - "github.com/hyperledger/firefly/mocks/tokenmocks" "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" @@ -46,6 +46,8 @@ func TestGetTokenTransfers(t *testing.T) { mdi.On("GetTokenTransfers", context.Background(), f).Return([]*fftypes.TokenTransfer{}, nil, nil) _, _, err := am.GetTokenTransfers(context.Background(), "ns1", f) assert.NoError(t, err) + + mdi.AssertExpectations(t) } func TestGetTokenTransferByID(t *testing.T) { @@ -57,6 +59,8 @@ func TestGetTokenTransferByID(t *testing.T) { mdi.On("GetTokenTransfer", context.Background(), u).Return(&fftypes.TokenTransfer{}, nil) _, err := am.GetTokenTransferByID(context.Background(), "ns1", u.String()) assert.NoError(t, err) + + mdi.AssertExpectations(t) } func TestGetTokenTransferByIDBadID(t *testing.T) { @@ -78,22 +82,28 @@ func TestMintTokensSuccess(t *testing.T) { Pool: "pool1", } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + })).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestMintTokenUnknownConnectorSuccess(t *testing.T) { @@ -107,22 +117,28 @@ func TestMintTokenUnknownConnectorSuccess(t *testing.T) { Pool: "pool1", } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + })).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestMintTokenUnknownConnectorNoConnectors(t *testing.T) { @@ -138,9 +154,6 @@ func TestMintTokenUnknownConnectorNoConnectors(t *testing.T) { am.tokens = make(map[string]tokens.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.Regexp(t, "FF10292", err) } @@ -159,9 +172,6 @@ func TestMintTokenUnknownConnectorMultipleConnectors(t *testing.T) { am.tokens["magic-tokens"] = nil am.tokens["magic-tokens2"] = nil - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.Regexp(t, "FF10292", err) } @@ -177,9 +187,6 @@ func TestMintTokenUnknownConnectorBadNamespace(t *testing.T) { Pool: "pool1", } - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - _, err := am.MintTokens(context.Background(), "", mint, false) assert.Regexp(t, "FF10131", err) } @@ -201,6 +208,8 @@ func TestMintTokenBadConnector(t *testing.T) { _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.Regexp(t, "FF10272", err) + + mim.AssertExpectations(t) } func TestMintTokenUnknownPoolSuccess(t *testing.T) { @@ -214,17 +223,16 @@ func TestMintTokenUnknownPoolSuccess(t *testing.T) { } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) fb := database.TokenPoolQueryFactory.NewFilter(context.Background()) f := fb.And() f.Limit(1).Count(true) tokenPools := []*fftypes.TokenPool{ { - Name: "pool1", - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + Name: "pool1", + State: fftypes.TokenPoolStateConfirmed, }, } totalCount := int64(1) @@ -237,12 +245,19 @@ func TestMintTokenUnknownPoolSuccess(t *testing.T) { return info.Count && info.Limit == 1 }))).Return(tokenPools, filterResult, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(tokenPools[0], nil) - mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + })).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestMintTokenUnknownPoolNoPools(t *testing.T) { @@ -256,7 +271,6 @@ func TestMintTokenUnknownPoolNoPools(t *testing.T) { } mdi := am.database.(*databasemocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) fb := database.TokenPoolQueryFactory.NewFilter(context.Background()) f := fb.And() f.Limit(1).Count(true) @@ -265,7 +279,6 @@ func TestMintTokenUnknownPoolNoPools(t *testing.T) { filterResult := &database.FilterResult{ TotalCount: &totalCount, } - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPools", context.Background(), mock.MatchedBy((func(f database.AndFilter) bool { info, _ := f.Finalize() return info.Count && info.Limit == 1 @@ -273,6 +286,8 @@ func TestMintTokenUnknownPoolNoPools(t *testing.T) { _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.Regexp(t, "FF10292", err) + + mdi.AssertExpectations(t) } func TestMintTokenUnknownPoolMultiplePools(t *testing.T) { @@ -286,7 +301,6 @@ func TestMintTokenUnknownPoolMultiplePools(t *testing.T) { } mdi := am.database.(*databasemocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) fb := database.TokenPoolQueryFactory.NewFilter(context.Background()) f := fb.And() f.Limit(1).Count(true) @@ -302,15 +316,15 @@ func TestMintTokenUnknownPoolMultiplePools(t *testing.T) { filterResult := &database.FilterResult{ TotalCount: &totalCount, } - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPools", context.Background(), mock.MatchedBy((func(f database.AndFilter) bool { info, _ := f.Finalize() return info.Count && info.Limit == 1 }))).Return(tokenPools, filterResult, nil) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.Regexp(t, "FF10292", err) + + mdi.AssertExpectations(t) } func TestMintTokenUnknownPoolBadNamespace(t *testing.T) { @@ -323,9 +337,6 @@ func TestMintTokenUnknownPoolBadNamespace(t *testing.T) { }, } - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - _, err := am.MintTokens(context.Background(), "", mint, false) assert.Regexp(t, "FF10131", err) } @@ -341,12 +352,12 @@ func TestMintTokensGetPoolsError(t *testing.T) { } mdi := am.database.(*databasemocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPools", context.Background(), mock.Anything).Return(nil, nil, fmt.Errorf("pop")) _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) } func TestMintTokensBadPool(t *testing.T) { @@ -367,6 +378,9 @@ func TestMintTokensBadPool(t *testing.T) { _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) } func TestMintTokensIdentityFail(t *testing.T) { @@ -385,6 +399,8 @@ func TestMintTokensIdentityFail(t *testing.T) { _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.EqualError(t, err, "pop") + + mim.AssertExpectations(t) } func TestMintTokensFail(t *testing.T) { @@ -398,53 +414,28 @@ func TestMintTokensFail(t *testing.T) { Pool: "pool1", } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(fmt.Errorf("pop")) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mth.On("WriteOperationFailure", context.Background(), mock.Anything, fmt.Errorf("pop")) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + })).Return(fmt.Errorf("pop")) _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.EqualError(t, err, "pop") -} - -func TestMintTokensFailAndDbFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mint := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - Amount: *fftypes.NewFFBigInt(5), - }, - Pool: "pool1", - } - pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, - } - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mth := am.txHelper.(*txcommonmocks.Helper) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(fmt.Errorf("pop")) - mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) - mth.On("WriteOperationFailure", context.Background(), mock.Anything, fmt.Errorf("pop")) - _, err := am.MintTokens(context.Background(), "ns1", mint, false) - assert.EqualError(t, err, "pop") + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestMintTokensOperationFail(t *testing.T) { @@ -472,6 +463,10 @@ func TestMintTokensOperationFail(t *testing.T) { _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) } func TestMintTokensConfirm(t *testing.T) { @@ -485,19 +480,17 @@ func TestMintTokensConfirm(t *testing.T) { Pool: "pool1", } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) msa.On("WaitForTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). @@ -506,6 +499,9 @@ func TestMintTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + })).Return(fmt.Errorf("pop")) _, err := am.MintTokens(context.Background(), "ns1", mint, true) assert.NoError(t, err) @@ -513,7 +509,7 @@ func TestMintTokensConfirm(t *testing.T) { mdi.AssertExpectations(t) mdm.AssertExpectations(t) msa.AssertExpectations(t) - mti.AssertExpectations(t) + mom.AssertExpectations(t) } func TestBurnTokensSuccess(t *testing.T) { @@ -527,26 +523,28 @@ func TestBurnTokensSuccess(t *testing.T) { Pool: "pool1", } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("BurnTokens", context.Background(), mock.Anything, "F1", &burn.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "burn" + })).Return(nil) _, err := am.BurnTokens(context.Background(), "ns1", burn, false) assert.NoError(t, err) mim.AssertExpectations(t) mdi.AssertExpectations(t) - mti.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestBurnTokensIdentityFail(t *testing.T) { @@ -565,6 +563,8 @@ func TestBurnTokensIdentityFail(t *testing.T) { _, err := am.BurnTokens(context.Background(), "ns1", burn, false) assert.EqualError(t, err, "pop") + + mim.AssertExpectations(t) } func TestBurnTokensConfirm(t *testing.T) { @@ -578,19 +578,17 @@ func TestBurnTokensConfirm(t *testing.T) { Pool: "pool1", } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("BurnTokens", context.Background(), mock.Anything, "F1", &burn.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) msa.On("WaitForTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). @@ -599,6 +597,9 @@ func TestBurnTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "burn" + })).Return(nil) _, err := am.BurnTokens(context.Background(), "ns1", burn, true) assert.NoError(t, err) @@ -606,7 +607,8 @@ func TestBurnTokensConfirm(t *testing.T) { mdi.AssertExpectations(t) mdm.AssertExpectations(t) msa.AssertExpectations(t) - mti.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferTokensSuccess(t *testing.T) { @@ -622,26 +624,28 @@ func TestTransferTokensSuccess(t *testing.T) { Pool: "pool1", } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) assert.NoError(t, err) mim.AssertExpectations(t) mdi.AssertExpectations(t) - mti.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferTokensUnconfirmedPool(t *testing.T) { @@ -691,6 +695,8 @@ func TestTransferTokensIdentityFail(t *testing.T) { _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) assert.EqualError(t, err, "pop") + + mim.AssertExpectations(t) } func TestTransferTokensNoFromOrTo(t *testing.T) { @@ -710,40 +716,6 @@ func TestTransferTokensNoFromOrTo(t *testing.T) { mim.AssertExpectations(t) } -func TestTransferTokensInvalidType(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - Connector: "magic-tokens", - Amount: *fftypes.NewFFBigInt(5), - }, - Pool: "pool1", - } - pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, - } - - mdi := am.database.(*databasemocks.Plugin) - mth := am.txHelper.(*txcommonmocks.Helper) - mdi.On("GetTokenPool", am.ctx, "ns1", "pool1").Return(pool, nil) - mth.On("SubmitNewTransaction", am.ctx, "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) - mdi.On("InsertOperation", am.ctx, mock.Anything).Return(nil) - - sender := &transferSender{ - mgr: am, - namespace: "ns1", - transfer: transfer, - } - assert.PanicsWithValue(t, "unknown transfer type: ", func() { - sender.Send(am.ctx) - }) -} - func TestTransferTokensTransactionFail(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() @@ -773,6 +745,7 @@ func TestTransferTokensTransactionFail(t *testing.T) { mim.AssertExpectations(t) mdi.AssertExpectations(t) + mth.AssertExpectations(t) } func TestTransferTokensWithBroadcastMessage(t *testing.T) { @@ -803,19 +776,17 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { }, } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mbm := am.broadcast.(*broadcastmocks.Manager) mms := &sysmessagingmocks.MessageSender{} mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) @@ -823,6 +794,9 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { mdi.On("UpsertMessage", context.Background(), mock.MatchedBy(func(msg *fftypes.Message) bool { return msg.State == fftypes.MessageStateStaged }), database.UpsertOptimizationNew).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) assert.NoError(t, err) @@ -832,8 +806,9 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { mbm.AssertExpectations(t) mim.AssertExpectations(t) mdi.AssertExpectations(t) - mti.AssertExpectations(t) mms.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferTokensWithBroadcastPrepareFail(t *testing.T) { @@ -900,19 +875,17 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { }, } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mpm := am.messaging.(*privatemessagingmocks.Manager) mms := &sysmessagingmocks.MessageSender{} mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) mpm.On("NewMessage", "ns1", transfer.Message).Return(mms) @@ -920,6 +893,9 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { mdi.On("UpsertMessage", context.Background(), mock.MatchedBy(func(msg *fftypes.Message) bool { return msg.State == fftypes.MessageStateStaged }), database.UpsertOptimizationNew).Return(nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) assert.NoError(t, err) @@ -929,8 +905,9 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { mpm.AssertExpectations(t) mim.AssertExpectations(t) mdi.AssertExpectations(t) - mti.AssertExpectations(t) mms.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferTokensWithInvalidMessage(t *testing.T) { @@ -980,19 +957,17 @@ func TestTransferTokensConfirm(t *testing.T) { Pool: "pool1", } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) msa.On("WaitForTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). @@ -1001,6 +976,9 @@ func TestTransferTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, true) assert.NoError(t, err) @@ -1008,7 +986,9 @@ func TestTransferTokensConfirm(t *testing.T) { mdi.AssertExpectations(t) mdm.AssertExpectations(t) msa.AssertExpectations(t) - mti.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferTokensWithBroadcastConfirm(t *testing.T) { @@ -1039,20 +1019,18 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { }, } pool := &fftypes.TokenPool{ - ProtocolID: "F1", - State: fftypes.TokenPoolStateConfirmed, + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mbm := am.broadcast.(*broadcastmocks.Manager) mms := &sysmessagingmocks.MessageSender{} msa := am.syncasync.(*syncasyncmocks.Bridge) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) @@ -1072,6 +1050,9 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { send(context.Background()) }). Return(&transfer.TokenTransfer, nil) + mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, true) assert.NoError(t, err) @@ -1081,9 +1062,9 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { mbm.AssertExpectations(t) mim.AssertExpectations(t) mdi.AssertExpectations(t) - mti.AssertExpectations(t) mms.AssertExpectations(t) msa.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferTokensPoolNotFound(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index 90a71edca1..ec142222ec 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -218,12 +218,6 @@ var ( SubscriptionsRetryMaxDelay = rootKey("subscription.retry.maxDelay") // SubscriptionsRetryFactor the backoff factor to use for retry of database operations SubscriptionsRetryFactor = rootKey("subscription.retry.factor") - // AssetManagerRetryInitialDelay is the initial retry delay - AssetManagerRetryInitialDelay = rootKey("asset.manager.retry.initDelay") - // AssetManagerRetryMaxDelay is the initial retry delay - AssetManagerRetryMaxDelay = rootKey("asset.manager.retry.maxDelay") - // AssetManagerRetryFactor the backoff factor to use for retry of database operations - AssetManagerRetryFactor = rootKey("asset.manager.retry.factor") // UIEnabled set to false to disable the UI (default is true, so UI will be enabled if ui.path is valid) UIEnabled = rootKey("ui.enabled") // UIPath the path on which to serve the UI @@ -351,9 +345,6 @@ func Reset() { viper.SetDefault(string(SubscriptionsRetryInitialDelay), "250ms") viper.SetDefault(string(SubscriptionsRetryMaxDelay), "30s") viper.SetDefault(string(SubscriptionsRetryFactor), 2.0) - viper.SetDefault(string(AssetManagerRetryInitialDelay), "250ms") - viper.SetDefault(string(AssetManagerRetryMaxDelay), "30s") - viper.SetDefault(string(AssetManagerRetryFactor), 2.0) viper.SetDefault(string(UIEnabled), true) viper.SetDefault(string(ValidatorCacheSize), "1Mb") viper.SetDefault(string(ValidatorCacheTTL), "1h") diff --git a/internal/definitions/definition_handler_tokenpool.go b/internal/definitions/definition_handler_tokenpool.go index 071b901a6b..a7bf3bf636 100644 --- a/internal/definitions/definition_handler_tokenpool.go +++ b/internal/definitions/definition_handler_tokenpool.go @@ -26,8 +26,6 @@ import ( func (dh *definitionHandlers) persistTokenPool(ctx context.Context, announce *fftypes.TokenPoolAnnouncement) (valid bool, err error) { pool := announce.Pool - - // Create the pool in pending state pool.State = fftypes.TokenPoolStatePending err = dh.database.UpsertTokenPool(ctx, pool) if err != nil { @@ -38,7 +36,6 @@ func (dh *definitionHandlers) persistTokenPool(ctx context.Context, announce *ff log.L(ctx).Errorf("Failed to insert token pool '%s': %s", pool.ID, err) return false, err // retryable } - return true, nil } @@ -63,6 +60,7 @@ func (dh *definitionHandlers) handleTokenPoolBroadcast(ctx context.Context, msg return ActionConfirm, nil, nil } + // Create the pool in pending state if valid, err := dh.persistTokenPool(ctx, &announce); err != nil { return ActionRetry, nil, err } else if !valid { @@ -73,7 +71,7 @@ func (dh *definitionHandlers) handleTokenPoolBroadcast(ctx context.Context, msg // This will ultimately trigger a pool creation event and a rewind return ActionWait, &DefinitionBatchActions{ PreFinalize: func(ctx context.Context) error { - if err := dh.assets.ActivateTokenPool(ctx, pool, announce.Event); err != nil { + if err := dh.assets.ActivateTokenPool(ctx, pool, announce.Event.Info); err != nil { log.L(ctx).Errorf("Failed to activate token pool '%s': %s", pool.ID, err) return err } diff --git a/internal/definitions/definition_handler_tokenpool_test.go b/internal/definitions/definition_handler_tokenpool_test.go index 9ae2088619..6d130dfb4d 100644 --- a/internal/definitions/definition_handler_tokenpool_test.go +++ b/internal/definitions/definition_handler_tokenpool_test.go @@ -44,8 +44,10 @@ func newPoolAnnouncement() *fftypes.TokenPoolAnnouncement { }, } return &fftypes.TokenPoolAnnouncement{ - Pool: pool, - Event: &fftypes.BlockchainEvent{}, + Pool: pool, + Event: &fftypes.BlockchainEvent{ + Info: fftypes.JSONObject{"some": "info"}, + }, } } @@ -80,7 +82,7 @@ func TestHandleDefinitionBroadcastTokenPoolActivateOK(t *testing.T) { mdi.On("UpsertTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { return *p.ID == *pool.ID && p.Message == msg.Header.ID })).Return(nil) - mam.On("ActivateTokenPool", context.Background(), mock.AnythingOfType("*fftypes.TokenPool"), mock.AnythingOfType("*fftypes.BlockchainEvent")).Return(nil) + mam.On("ActivateTokenPool", context.Background(), mock.AnythingOfType("*fftypes.TokenPool"), announce.Event.Info).Return(nil) action, ba, err := sh.HandleDefinitionBroadcast(context.Background(), msg, data) assert.Equal(t, ActionWait, action) @@ -210,7 +212,7 @@ func TestHandleDefinitionBroadcastTokenPoolActivateFail(t *testing.T) { mdi.On("UpsertTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { return *p.ID == *pool.ID && p.Message == msg.Header.ID })).Return(nil) - mam.On("ActivateTokenPool", context.Background(), mock.AnythingOfType("*fftypes.TokenPool"), mock.AnythingOfType("*fftypes.BlockchainEvent")).Return(fmt.Errorf("pop")) + mam.On("ActivateTokenPool", context.Background(), mock.AnythingOfType("*fftypes.TokenPool"), announce.Event.Info).Return(fmt.Errorf("pop")) action, batchAction, err := sh.HandleDefinitionBroadcast(context.Background(), msg, data) assert.Equal(t, ActionWait, action) diff --git a/internal/events/operation_update.go b/internal/events/operation_update.go index 0f1ee8356a..1d9abc315b 100644 --- a/internal/events/operation_update.go +++ b/internal/events/operation_update.go @@ -27,7 +27,7 @@ import ( func (em *eventManager) operationUpdateCtx(ctx context.Context, operationID *fftypes.UUID, txState fftypes.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) error { op, err := em.database.GetOperationByID(ctx, operationID) if err != nil || op == nil { - log.L(em.ctx).Warnf("Operation update '%s' ignored, as it was not submitted by this node", operationID) + log.L(ctx).Warnf("Operation update '%s' ignored, as it was not submitted by this node", operationID) return nil } @@ -39,12 +39,11 @@ func (em *eventManager) operationUpdateCtx(ctx context.Context, operationID *fft if op.Type == fftypes.OpTypeTokenTransfer && txState == fftypes.OpStatusFailed { event := fftypes.NewEvent(fftypes.EventTypeTransferOpFailed, op.Namespace, op.ID) if em.metrics.IsMetricsEnabled() { - var tokenTransfer fftypes.TokenTransfer - err = txcommon.RetrieveTokenTransferInputs(ctx, op, &tokenTransfer) - if err != nil { - log.L(em.ctx).Warnf("Could not determine token transfer type: %s", err) + tokenTransfer, err := txcommon.RetrieveTokenTransferInputs(ctx, op) + if err != nil || tokenTransfer.LocalID == nil || tokenTransfer.Type == "" { + log.L(ctx).Warnf("Could not determine token transfer type: %s", err) } - em.metrics.TransferConfirmed(&tokenTransfer) + em.metrics.TransferConfirmed(tokenTransfer) } if err := em.database.InsertEvent(ctx, event); err != nil { return err diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go index 0a9330c175..fbf462ee76 100644 --- a/internal/events/token_pool_created.go +++ b/internal/events/token_pool_created.go @@ -89,8 +89,7 @@ func (em *eventManager) shouldConfirm(ctx context.Context, pool *tokens.TokenPoo // Unknown pool state - should only happen on first run after database migration // Activate the pool, then immediately confirm // TODO: can this state eventually be removed? - ev := buildBlockchainEvent(existingPool.Namespace, nil, &pool.Event, &existingPool.TX) - if err = em.assets.ActivateTokenPool(ctx, existingPool, ev); err != nil { + if err = em.assets.ActivateTokenPool(ctx, existingPool, pool.Event.Info); err != nil { log.L(ctx).Errorf("Failed to activate token pool '%s': %s", existingPool.ID, err) return nil, err } @@ -106,8 +105,8 @@ func (em *eventManager) shouldAnnounce(ctx context.Context, pool *tokens.TokenPo return nil, nil } - announcePool = &fftypes.TokenPool{} - if err = txcommon.RetrieveTokenPoolCreateInputs(ctx, op, announcePool); err != nil { + announcePool, err = txcommon.RetrieveTokenPoolCreateInputs(ctx, op) + if err != nil || announcePool.ID == nil || announcePool.Namespace == "" || announcePool.Name == "" { log.L(ctx).Errorf("Error loading pool info for transaction '%s' (%s) - ignoring: %v", pool.TransactionID, err, op.Input) return nil, nil } diff --git a/internal/events/token_pool_created_test.go b/internal/events/token_pool_created_test.go index e02c074ba3..7998d2d6bb 100644 --- a/internal/events/token_pool_created_test.go +++ b/internal/events/token_pool_created_test.go @@ -233,12 +233,8 @@ func TestTokenPoolCreatedMigrate(t *testing.T) { mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypePoolConfirmed && *e.Reference == *storedPool.ID })).Return(nil).Once() - mam.On("ActivateTokenPool", em.ctx, storedPool, mock.MatchedBy(func(e *fftypes.BlockchainEvent) bool { - return e.ProtocolID == chainPool.Event.ProtocolID - })).Return(fmt.Errorf("pop")).Once() - mam.On("ActivateTokenPool", em.ctx, storedPool, mock.MatchedBy(func(e *fftypes.BlockchainEvent) bool { - return e.ProtocolID == chainPool.Event.ProtocolID - })).Return(nil).Once() + mam.On("ActivateTokenPool", em.ctx, storedPool, info).Return(fmt.Errorf("pop")).Once() + mam.On("ActivateTokenPool", em.ctx, storedPool, info).Return(nil).Once() mdi.On("GetMessageByID", em.ctx, storedPool.Message).Return(storedMessage, nil) err := em.TokenPoolCreated(mti, chainPool) diff --git a/internal/events/tokens_transferred.go b/internal/events/tokens_transferred.go index 9d8c7e02d8..107950daf3 100644 --- a/internal/events/tokens_transferred.go +++ b/internal/events/tokens_transferred.go @@ -40,8 +40,10 @@ func (em *eventManager) loadTransferOperation(ctx context.Context, tx *fftypes.U return err } if len(operations) > 0 { - if err = txcommon.RetrieveTokenTransferInputs(ctx, operations[0], transfer); err != nil { + if origTransfer, err := txcommon.RetrieveTokenTransferInputs(ctx, operations[0]); err != nil { log.L(ctx).Warnf("Failed to read operation inputs for token transfer '%s': %s", transfer.ProtocolID, err) + } else if origTransfer != nil { + transfer.LocalID = origTransfer.LocalID } } diff --git a/internal/i18n/en_translations.go b/internal/i18n/en_translations.go index 26e15de9bc..c6f3c67402 100644 --- a/internal/i18n/en_translations.go +++ b/internal/i18n/en_translations.go @@ -263,4 +263,5 @@ var ( MsgInvalidTXTypeForMessage = ffm("FF10343", "Invalid transaction type for sending a message: %s", 400) MsgGroupRequired = ffm("FF10344", "Group must be set", 400) MsgDBLockFailed = ffm("FF10345", "Database lock failed") + MsgOperationNotSupported = ffm("FF10346", "Operation not supported", 400) ) diff --git a/internal/operations/manager.go b/internal/operations/manager.go new file mode 100644 index 0000000000..715638617b --- /dev/null +++ b/internal/operations/manager.go @@ -0,0 +1,151 @@ +// Copyright © 2022 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 operations + +import ( + "context" + "fmt" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/internal/txcommon" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/hyperledger/firefly/pkg/tokens" +) + +type Manager interface { + StartOperation(ctx context.Context, op *fftypes.Operation) error +} + +type operationsManager struct { + ctx context.Context + database database.Plugin + txHelper txcommon.Helper + tokens map[string]tokens.Plugin +} + +func NewOperationsManager(ctx context.Context, di database.Plugin, ti map[string]tokens.Plugin) (Manager, error) { + if di == nil || ti == nil { + return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) + } + om := &operationsManager{ + ctx: ctx, + database: di, + txHelper: txcommon.NewTransactionHelper(di), + tokens: ti, + } + return om, nil +} + +func (om *operationsManager) StartOperation(ctx context.Context, op *fftypes.Operation) error { + switch op.Type { + case fftypes.OpTypeTokenCreatePool: + pool, err := txcommon.RetrieveTokenPoolCreateInputs(ctx, op) + if err != nil { + return err + } + return om.createTokenPool(ctx, op.ID, pool) + + case fftypes.OpTypeTokenActivatePool: + poolID, blockchainInfo, err := txcommon.RetrieveTokenPoolActivateInputs(ctx, op) + if err != nil { + return err + } + pool, err := om.database.GetTokenPoolByID(ctx, poolID) + if err != nil { + return err + } else if pool == nil { + return i18n.NewError(ctx, i18n.Msg404NotFound) + } + return om.activateTokenPool(ctx, op.ID, pool, blockchainInfo) + + case fftypes.OpTypeTokenTransfer: + transfer, err := txcommon.RetrieveTokenTransferInputs(ctx, op) + if err != nil { + return err + } + pool, err := om.database.GetTokenPoolByID(ctx, transfer.Pool) + if err != nil { + return err + } else if pool == nil { + return i18n.NewError(ctx, i18n.Msg404NotFound) + } + return om.transferTokens(ctx, op.ID, pool.ProtocolID, transfer) + + default: + return i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func (om *operationsManager) selectTokenPlugin(ctx context.Context, name string) (tokens.Plugin, error) { + for pluginName, plugin := range om.tokens { + if pluginName == name { + return plugin, nil + } + } + return nil, i18n.NewError(ctx, i18n.MsgUnknownTokensPlugin, name) +} + +func (om *operationsManager) createTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool) error { + plugin, err := om.selectTokenPlugin(ctx, pool.Connector) + if err != nil { + return err + } + if complete, err := plugin.CreateTokenPool(ctx, opID, pool); err != nil { + om.txHelper.WriteOperationFailure(ctx, opID, err) + return err + } else if complete { + om.txHelper.WriteOperationSuccess(ctx, opID, nil) + } + return nil +} + +func (om *operationsManager) activateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) error { + plugin, err := om.selectTokenPlugin(ctx, pool.Connector) + if err != nil { + return err + } + if complete, err := plugin.ActivateTokenPool(ctx, opID, pool, blockchainInfo); err != nil { + om.txHelper.WriteOperationFailure(ctx, opID, err) + return err + } else if complete { + om.txHelper.WriteOperationSuccess(ctx, opID, nil) + } + return nil +} + +func (om *operationsManager) transferTokens(ctx context.Context, opID *fftypes.UUID, poolProtocolID string, transfer *fftypes.TokenTransfer) error { + plugin, err := om.selectTokenPlugin(ctx, transfer.Connector) + if err != nil { + return err + } + switch transfer.Type { + case fftypes.TokenTransferTypeMint: + err = plugin.MintTokens(ctx, opID, poolProtocolID, transfer) + case fftypes.TokenTransferTypeTransfer: + err = plugin.TransferTokens(ctx, opID, poolProtocolID, transfer) + case fftypes.TokenTransferTypeBurn: + err = plugin.BurnTokens(ctx, opID, poolProtocolID, transfer) + default: + panic(fmt.Sprintf("unknown transfer type: %v", transfer.Type)) + } + if err != nil { + om.txHelper.WriteOperationFailure(ctx, opID, err) + return err + } + return nil +} diff --git a/internal/operations/manager_test.go b/internal/operations/manager_test.go new file mode 100644 index 0000000000..c496796270 --- /dev/null +++ b/internal/operations/manager_test.go @@ -0,0 +1,48 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance 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 imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package operations + +import ( + "context" + "testing" + + "github.com/hyperledger/firefly/internal/config" + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/tokenmocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/hyperledger/firefly/pkg/tokens" + "github.com/stretchr/testify/assert" +) + +func newTestOperations(t *testing.T) (*operationsManager, func()) { + config.Reset() + mdi := &databasemocks.Plugin{} + mti := &tokenmocks.Plugin{} + mti.On("Name").Return("ut_tokens").Maybe() + ctx, cancel := context.WithCancel(context.Background()) + om, err := NewOperationsManager(ctx, mdi, map[string]tokens.Plugin{"magic-tokens": mti}) + assert.NoError(t, err) + return om.(*operationsManager), cancel +} + +func TestStartOperationNotSupported(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + op := &fftypes.Operation{} + + err := om.StartOperation(context.Background(), op) + assert.Regexp(t, "FF10346", err) +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index b7fae5f0a6..406cc37904 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -38,6 +38,7 @@ import ( "github.com/hyperledger/firefly/internal/log" "github.com/hyperledger/firefly/internal/metrics" "github.com/hyperledger/firefly/internal/networkmap" + "github.com/hyperledger/firefly/internal/operations" "github.com/hyperledger/firefly/internal/privatemessaging" "github.com/hyperledger/firefly/internal/publicstorage/psfactory" "github.com/hyperledger/firefly/internal/syncasync" @@ -158,6 +159,7 @@ type orchestrator struct { contracts contracts.Manager node *fftypes.UUID metrics metrics.Manager + operations operations.Manager } func NewOrchestrator() Orchestrator { @@ -446,6 +448,12 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) { or.syncasync = syncasync.NewSyncAsyncBridge(ctx, or.database, or.data) or.batchpin = batchpin.NewBatchPinSubmitter(or.database, or.identity, or.blockchain, or.metrics) + if or.operations == nil { + if or.operations, err = operations.NewOperationsManager(ctx, or.database, or.tokens); err != nil { + return err + } + } + if or.messaging == nil { if or.messaging, err = privatemessaging.NewPrivateMessaging(ctx, or.database, or.identity, or.dataexchange, or.blockchain, or.batch, or.data, or.syncasync, or.batchpin, or.metrics); err != nil { return err @@ -459,7 +467,7 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) { } if or.assets == nil { - or.assets, err = assets.NewAssetManager(ctx, or.database, or.identity, or.data, or.syncasync, or.broadcast, or.messaging, or.tokens, or.metrics) + or.assets, err = assets.NewAssetManager(ctx, or.database, or.identity, or.data, or.syncasync, or.broadcast, or.messaging, or.tokens, or.metrics, or.operations) if err != nil { return err } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 74f2f4456a..c6e8888738 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -38,6 +38,7 @@ import ( "github.com/hyperledger/firefly/mocks/identitymocks" "github.com/hyperledger/firefly/mocks/metricsmocks" "github.com/hyperledger/firefly/mocks/networkmapmocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/mocks/privatemessagingmocks" "github.com/hyperledger/firefly/mocks/publicstoragemocks" "github.com/hyperledger/firefly/mocks/tokenmocks" @@ -68,6 +69,7 @@ type testOrchestrator struct { mti *tokenmocks.Plugin mcm *contractmocks.Manager mmi *metricsmocks.Manager + mom *operationmocks.Manager } func newTestOrchestrator() *testOrchestrator { @@ -94,6 +96,7 @@ func newTestOrchestrator() *testOrchestrator { mti: &tokenmocks.Plugin{}, mcm: &contractmocks.Manager{}, mmi: &metricsmocks.Manager{}, + mom: &operationmocks.Manager{}, } tor.orchestrator.database = tor.mdi tor.orchestrator.data = tor.mdm @@ -111,6 +114,7 @@ func newTestOrchestrator() *testOrchestrator { tor.orchestrator.contracts = tor.mcm tor.orchestrator.tokens = map[string]tokens.Plugin{"token": tor.mti} tor.orchestrator.metrics = tor.mmi + tor.orchestrator.operations = tor.mom tor.mdi.On("Name").Return("mock-di").Maybe() tor.mem.On("Name").Return("mock-ei").Maybe() tor.mps.On("Name").Return("mock-ps").Maybe() @@ -511,6 +515,14 @@ func TestInitContractsComponentFail(t *testing.T) { assert.Regexp(t, "FF10128", err) } +func TestInitOperationsComponentFail(t *testing.T) { + or := newTestOrchestrator() + or.database = nil + or.operations = nil + err := or.initComponents(context.Background()) + assert.Regexp(t, "FF10128", err) +} + func TestStartBatchFail(t *testing.T) { config.Reset() or := newTestOrchestrator() diff --git a/internal/syncasync/sync_async_bridge.go b/internal/syncasync/sync_async_bridge.go index d7683995eb..0d100a2b5d 100644 --- a/internal/syncasync/sync_async_bridge.go +++ b/internal/syncasync/sync_async_bridge.go @@ -287,8 +287,8 @@ func (sa *syncAsyncBridge) eventCallback(event *fftypes.EventDelivery) error { return err } // Extract the LocalID of the transfer - var transfer fftypes.TokenTransfer - if err := txcommon.RetrieveTokenTransferInputs(sa.ctx, op, &transfer); err != nil { + transfer, err := txcommon.RetrieveTokenTransferInputs(sa.ctx, op) + if err != nil || transfer.LocalID == nil { log.L(sa.ctx).Warnf("Failed to extract token transfer inputs for operation '%s': %s", op.ID, err) } // See if this is a failure of an inflight token transfer operation diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go index f8b4609fea..eeee0c0e93 100644 --- a/internal/tokens/fftokens/fftokens.go +++ b/internal/tokens/fftokens/fftokens.go @@ -399,12 +399,12 @@ func (ft *FFTokens) CreateTokenPool(ctx context.Context, opID *fftypes.UUID, poo return false, nil } -func (ft *FFTokens) ActivateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool, event *fftypes.BlockchainEvent) (complete bool, err error) { +func (ft *FFTokens) ActivateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) (complete bool, err error) { res, err := ft.client.R().SetContext(ctx). SetBody(&activatePool{ RequestID: opID.String(), PoolID: pool.ProtocolID, - Transaction: event.Info, + Transaction: blockchainInfo, }). Post("/api/v1/activatepool") if err != nil || !res.IsSuccess() { diff --git a/internal/tokens/fftokens/fftokens_test.go b/internal/tokens/fftokens/fftokens_test.go index 75746dea38..1e35672f2f 100644 --- a/internal/tokens/fftokens/fftokens_test.go +++ b/internal/tokens/fftokens/fftokens_test.go @@ -274,9 +274,6 @@ func TestActivateTokenPool(t *testing.T) { txInfo := map[string]interface{}{ "foo": "bar", } - ev := &fftypes.BlockchainEvent{ - Info: txInfo, - } httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/activatepool", httpURL), func(req *http.Request) (*http.Response, error) { @@ -299,7 +296,7 @@ func TestActivateTokenPool(t *testing.T) { return res, nil }) - complete, err := h.ActivateTokenPool(context.Background(), opID, pool, ev) + complete, err := h.ActivateTokenPool(context.Background(), opID, pool, txInfo) assert.False(t, complete) assert.NoError(t, err) } @@ -315,12 +312,11 @@ func TestActivateTokenPoolError(t *testing.T) { Type: fftypes.TransactionTypeTokenPool, }, } - ev := &fftypes.BlockchainEvent{} httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/activatepool", httpURL), httpmock.NewJsonResponderOrPanic(500, fftypes.JSONObject{})) - complete, err := h.ActivateTokenPool(context.Background(), fftypes.NewUUID(), pool, ev) + complete, err := h.ActivateTokenPool(context.Background(), fftypes.NewUUID(), pool, nil) assert.False(t, complete) assert.Regexp(t, "FF10274", err) } @@ -336,9 +332,6 @@ func TestActivateTokenPoolSynchronous(t *testing.T) { txInfo := map[string]interface{}{ "foo": "bar", } - ev := &fftypes.BlockchainEvent{ - Info: txInfo, - } httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/activatepool", httpURL), func(req *http.Request) (*http.Response, error) { @@ -370,7 +363,7 @@ func TestActivateTokenPoolSynchronous(t *testing.T) { return p.ProtocolID == "F1" && p.Type == fftypes.TokenTypeFungible && p.TransactionID == nil && p.Event.ProtocolID == "" })).Return(nil) - complete, err := h.ActivateTokenPool(context.Background(), opID, pool, ev) + complete, err := h.ActivateTokenPool(context.Background(), opID, pool, txInfo) assert.True(t, complete) assert.NoError(t, err) } @@ -386,9 +379,6 @@ func TestActivateTokenPoolSynchronousBadResponse(t *testing.T) { txInfo := map[string]interface{}{ "foo": "bar", } - ev := &fftypes.BlockchainEvent{ - Info: txInfo, - } httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/activatepool", httpURL), func(req *http.Request) (*http.Response, error) { @@ -416,7 +406,7 @@ func TestActivateTokenPoolSynchronousBadResponse(t *testing.T) { return p.ProtocolID == "F1" && p.Type == fftypes.TokenTypeFungible && p.TransactionID == nil && p.Event.ProtocolID == "" })).Return(nil) - complete, err := h.ActivateTokenPool(context.Background(), opID, pool, ev) + complete, err := h.ActivateTokenPool(context.Background(), opID, pool, txInfo) assert.False(t, complete) assert.Regexp(t, "FF10151", err) } diff --git a/internal/txcommon/token_inputs.go b/internal/txcommon/token_inputs.go index b173b67dcd..4643e49917 100644 --- a/internal/txcommon/token_inputs.go +++ b/internal/txcommon/token_inputs.go @@ -19,57 +19,54 @@ package txcommon import ( "context" "encoding/json" - "fmt" "github.com/hyperledger/firefly/internal/i18n" "github.com/hyperledger/firefly/pkg/fftypes" ) -func AddTokenPoolCreateInputs(op *fftypes.Operation, pool *fftypes.TokenPool) { - op.Input = fftypes.JSONObject{ - "id": pool.ID.String(), - "namespace": pool.Namespace, - "name": pool.Name, - "symbol": pool.Symbol, - "config": pool.Config, +func AddTokenPoolCreateInputs(op *fftypes.Operation, pool *fftypes.TokenPool) (err error) { + var poolJSON []byte + if poolJSON, err = json.Marshal(pool); err == nil { + err = json.Unmarshal(poolJSON, &op.Input) } + return err } -func RetrieveTokenPoolCreateInputs(ctx context.Context, op *fftypes.Operation, pool *fftypes.TokenPool) (err error) { - input := &op.Input - pool.ID, err = fftypes.ParseUUID(ctx, input.GetString("id")) - if err != nil { - return err +func RetrieveTokenPoolCreateInputs(ctx context.Context, op *fftypes.Operation) (*fftypes.TokenPool, error) { + var pool fftypes.TokenPool + s := op.Input.String() + if err := json.Unmarshal([]byte(s), &pool); err != nil { + return nil, i18n.WrapError(ctx, err, i18n.MsgJSONObjectParseFailed, s) } - pool.Namespace = input.GetString("namespace") - pool.Name = input.GetString("name") - if pool.Namespace == "" || pool.Name == "" { - return fmt.Errorf("namespace or name missing from inputs") + return &pool, nil +} + +func AddTokenPoolActivateInputs(op *fftypes.Operation, poolID *fftypes.UUID, blockchainInfo fftypes.JSONObject) { + op.Input = fftypes.JSONObject{ + "id": poolID.String(), + "info": blockchainInfo, } - pool.Symbol = input.GetString("symbol") - pool.Config = input.GetObject("config") - return nil +} + +func RetrieveTokenPoolActivateInputs(ctx context.Context, op *fftypes.Operation) (*fftypes.UUID, fftypes.JSONObject, error) { + id, err := fftypes.ParseUUID(ctx, op.Input.GetString("id")) + info := op.Input.GetObject("info") + return id, info, err } func AddTokenTransferInputs(op *fftypes.Operation, transfer *fftypes.TokenTransfer) (err error) { - var j []byte - if j, err = json.Marshal(transfer); err == nil { - err = json.Unmarshal(j, &op.Input) + var transferJSON []byte + if transferJSON, err = json.Marshal(transfer); err == nil { + err = json.Unmarshal(transferJSON, &op.Input) } return err } -func RetrieveTokenTransferInputs(ctx context.Context, op *fftypes.Operation, transfer *fftypes.TokenTransfer) (err error) { - var t fftypes.TokenTransfer +func RetrieveTokenTransferInputs(ctx context.Context, op *fftypes.Operation) (*fftypes.TokenTransfer, error) { + var transfer fftypes.TokenTransfer s := op.Input.String() - if err = json.Unmarshal([]byte(s), &t); err != nil { - return i18n.WrapError(ctx, err, i18n.MsgJSONObjectParseFailed, s) - } - if t.LocalID == nil { - return i18n.NewError(ctx, i18n.MsgInvalidUUID) + if err := json.Unmarshal([]byte(s), &transfer); err != nil { + return nil, i18n.WrapError(ctx, err, i18n.MsgJSONObjectParseFailed, s) } - // The LocalID is the only thing that needs to be read back out when processing an event - // (everything else should be unpacked from the event) - transfer.LocalID = t.LocalID - return nil + return &transfer, nil } diff --git a/internal/txcommon/token_inputs_test.go b/internal/txcommon/token_inputs_test.go index 218a684cef..0fa245badb 100644 --- a/internal/txcommon/token_inputs_test.go +++ b/internal/txcommon/token_inputs_test.go @@ -59,9 +59,8 @@ func TestRetrieveTokenPoolCreateInputs(t *testing.T) { "config": config, }, } - pool := &fftypes.TokenPool{} - err := RetrieveTokenPoolCreateInputs(context.Background(), op, pool) + pool, err := RetrieveTokenPoolCreateInputs(context.Background(), op) assert.NoError(t, err) assert.Equal(t, *id, *pool.ID) assert.Equal(t, "ns1", pool.Namespace) @@ -76,23 +75,39 @@ func TestRetrieveTokenPoolCreateInputsBadID(t *testing.T) { "id": "bad", }, } - pool := &fftypes.TokenPool{} - err := RetrieveTokenPoolCreateInputs(context.Background(), op, pool) - assert.Regexp(t, "FF10142", err) + _, err := RetrieveTokenPoolCreateInputs(context.Background(), op) + assert.Regexp(t, "FF10151", err) +} + +func TestAddTokenPoolActivateInputs(t *testing.T) { + op := &fftypes.Operation{} + poolID := fftypes.NewUUID() + info := fftypes.JSONObject{ + "some": "info", + } + + AddTokenPoolActivateInputs(op, poolID, info) + assert.Equal(t, poolID.String(), op.Input.GetString("id")) + assert.Equal(t, info, op.Input.GetObject("info")) } -func TestRetrieveTokenPoolCreateInputsNoName(t *testing.T) { +func TestRetrieveTokenPoolActivateInputs(t *testing.T) { + id := fftypes.NewUUID() + info := fftypes.JSONObject{ + "foo": "bar", + } op := &fftypes.Operation{ Input: fftypes.JSONObject{ - "id": fftypes.NewUUID().String(), - "namespace": "ns1", + "id": id.String(), + "info": info, }, } - pool := &fftypes.TokenPool{} - err := RetrieveTokenPoolCreateInputs(context.Background(), op, pool) - assert.Error(t, err) + poolID, newInfo, err := RetrieveTokenPoolActivateInputs(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, *id, *poolID) + assert.Equal(t, info, newInfo) } func TestAddTokenTransferInputs(t *testing.T) { @@ -127,12 +142,11 @@ func TestRetrieveTokenTransferInputs(t *testing.T) { "localId": id.String(), }, } - transfer := &fftypes.TokenTransfer{Amount: *fftypes.NewFFBigInt(2)} - err := RetrieveTokenTransferInputs(context.Background(), op, transfer) + transfer, err := RetrieveTokenTransferInputs(context.Background(), op) assert.NoError(t, err) assert.Equal(t, *id, *transfer.LocalID) - assert.Equal(t, int64(2), transfer.Amount.Int().Int64()) + assert.Equal(t, int64(1), transfer.Amount.Int().Int64()) } func TestRetrieveTokenTransferInputsBadID(t *testing.T) { @@ -141,18 +155,7 @@ func TestRetrieveTokenTransferInputsBadID(t *testing.T) { "localId": "bad", }, } - transfer := &fftypes.TokenTransfer{} - err := RetrieveTokenTransferInputs(context.Background(), op, transfer) + _, err := RetrieveTokenTransferInputs(context.Background(), op) assert.Regexp(t, "FF10151", err) } - -func TestRetrieveTokenTransferInputsMissingID(t *testing.T) { - op := &fftypes.Operation{ - Input: fftypes.JSONObject{}, - } - transfer := &fftypes.TokenTransfer{} - - err := RetrieveTokenTransferInputs(context.Background(), op, transfer) - assert.Regexp(t, "FF10142", err) -} diff --git a/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index 726f7cf428..1301f8a26d 100644 --- a/mocks/assetmocks/manager.go +++ b/mocks/assetmocks/manager.go @@ -18,13 +18,13 @@ type Manager struct { mock.Mock } -// ActivateTokenPool provides a mock function with given fields: ctx, pool, event -func (_m *Manager) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, event *fftypes.BlockchainEvent) error { - ret := _m.Called(ctx, pool, event) +// ActivateTokenPool provides a mock function with given fields: ctx, pool, blockchainInfo +func (_m *Manager) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) error { + ret := _m.Called(ctx, pool, blockchainInfo) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.TokenPool, *fftypes.BlockchainEvent) error); ok { - r0 = rf(ctx, pool, event) + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.TokenPool, fftypes.JSONObject) error); ok { + r0 = rf(ctx, pool, blockchainInfo) } else { r0 = ret.Error(0) } @@ -369,20 +369,6 @@ func (_m *Manager) NewTransfer(ns string, transfer *fftypes.TokenTransferInput) return r0 } -// Start provides a mock function with given fields: -func (_m *Manager) Start() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - // TransferTokens provides a mock function with given fields: ctx, ns, transfer, waitConfirm func (_m *Manager) TransferTokens(ctx context.Context, ns string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (*fftypes.TokenTransfer, error) { ret := _m.Called(ctx, ns, transfer, waitConfirm) @@ -405,8 +391,3 @@ func (_m *Manager) TransferTokens(ctx context.Context, ns string, transfer *ffty return r0, r1 } - -// WaitStop provides a mock function with given fields: -func (_m *Manager) WaitStop() { - _m.Called() -} diff --git a/mocks/operationmocks/manager.go b/mocks/operationmocks/manager.go new file mode 100644 index 0000000000..5f53e8c005 --- /dev/null +++ b/mocks/operationmocks/manager.go @@ -0,0 +1,29 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package operationmocks + +import ( + context "context" + + fftypes "github.com/hyperledger/firefly/pkg/fftypes" + mock "github.com/stretchr/testify/mock" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// StartOperation provides a mock function with given fields: ctx, op +func (_m *Manager) StartOperation(ctx context.Context, op *fftypes.Operation) error { + ret := _m.Called(ctx, op) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) error); ok { + r0 = rf(ctx, op) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/tokenmocks/plugin.go b/mocks/tokenmocks/plugin.go index 24903df2f7..9f6d350c17 100644 --- a/mocks/tokenmocks/plugin.go +++ b/mocks/tokenmocks/plugin.go @@ -19,20 +19,20 @@ type Plugin struct { mock.Mock } -// ActivateTokenPool provides a mock function with given fields: ctx, opID, pool, event -func (_m *Plugin) ActivateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool, event *fftypes.BlockchainEvent) (bool, error) { - ret := _m.Called(ctx, opID, pool, event) +// ActivateTokenPool provides a mock function with given fields: ctx, opID, pool, blockchainInfo +func (_m *Plugin) ActivateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) (bool, error) { + ret := _m.Called(ctx, opID, pool, blockchainInfo) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, *fftypes.TokenPool, *fftypes.BlockchainEvent) bool); ok { - r0 = rf(ctx, opID, pool, event) + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, *fftypes.TokenPool, fftypes.JSONObject) bool); ok { + r0 = rf(ctx, opID, pool, blockchainInfo) } else { r0 = ret.Get(0).(bool) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, *fftypes.TokenPool, *fftypes.BlockchainEvent) error); ok { - r1 = rf(ctx, opID, pool, event) + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, *fftypes.TokenPool, fftypes.JSONObject) error); ok { + r1 = rf(ctx, opID, pool, blockchainInfo) } else { r1 = ret.Error(1) } diff --git a/pkg/tokens/plugin.go b/pkg/tokens/plugin.go index 28f824d08e..a98bf67ff2 100644 --- a/pkg/tokens/plugin.go +++ b/pkg/tokens/plugin.go @@ -45,7 +45,7 @@ type Plugin interface { CreateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool) (complete bool, err error) // ActivateTokenPool activates a pool in order to begin receiving events - ActivateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool, event *fftypes.BlockchainEvent) (complete bool, err error) + ActivateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) (complete bool, err error) // MintTokens mints new tokens in a pool and adds them to the recipient's account MintTokens(ctx context.Context, opID *fftypes.UUID, poolProtocolID string, mint *fftypes.TokenTransfer) error From e03289dc77c5c36fff7d19d7806cdb340931701d Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 15 Feb 2022 21:21:06 -0500 Subject: [PATCH 02/10] Implement a registry in Operations Manager Other managers can register to handle particular operation types. This keeps the logic for each type concentrated in the owning Manager instead of giving too much specialized knowledge to the Operations Manager. Also introduce a serializable PreparedOperation type for wrapping operations before they are sent off to plugins - makes for a neater split between parsing and running operations, and may also be useful for tests/debugging later. Signed-off-by: Andrew Richardson --- internal/assets/manager.go | 9 ++ internal/assets/manager_test.go | 2 + internal/assets/operations.go | 141 +++++++++++++++++++++++++ internal/assets/token_pool.go | 4 +- internal/assets/token_pool_test.go | 46 ++++---- internal/assets/token_transfer.go | 5 +- internal/assets/token_transfer_test.go | 60 ++++++----- internal/operations/manager.go | 113 +++++--------------- internal/operations/manager_test.go | 4 +- mocks/assetmocks/manager.go | 44 ++++++++ mocks/operationmocks/manager.go | 36 ++++++- pkg/fftypes/operation.go | 12 ++- 12 files changed, 335 insertions(+), 141 deletions(-) create mode 100644 internal/assets/operations.go diff --git a/internal/assets/manager.go b/internal/assets/manager.go index cbf731ce45..3869448736 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -54,6 +54,10 @@ type Manager interface { TransferTokens(ctx context.Context, ns string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (*fftypes.TokenTransfer, error) GetTokenConnectors(ctx context.Context, ns string) ([]*fftypes.TokenConnector, error) + + // From operations.OperationHandler + PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) + RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) } type assetManager struct { @@ -87,6 +91,11 @@ func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manage metrics: mm, operations: ops, } + ops.RegisterHandler(am, []fftypes.OpType{ + fftypes.OpTypeTokenCreatePool, + fftypes.OpTypeTokenActivatePool, + fftypes.OpTypeTokenTransfer, + }) return am, nil } diff --git a/internal/assets/manager_test.go b/internal/assets/manager_test.go index 92fc02451a..eca5d1a248 100644 --- a/internal/assets/manager_test.go +++ b/internal/assets/manager_test.go @@ -49,6 +49,7 @@ func newTestAssets(t *testing.T) (*assetManager, func()) { mom := &operationmocks.Manager{} mti.On("Name").Return("ut_tokens").Maybe() mm.On("IsMetricsEnabled").Return(false) + mom.On("RegisterHandler", mock.Anything, mock.Anything) ctx, cancel := context.WithCancel(context.Background()) a, err := NewAssetManager(ctx, mdi, mim, mdm, msa, mbm, mpm, map[string]tokens.Plugin{"magic-tokens": mti}, mm, mom) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() @@ -75,6 +76,7 @@ func newTestAssetsWithMetrics(t *testing.T) (*assetManager, func()) { mti.On("Name").Return("ut_tokens").Maybe() mm.On("IsMetricsEnabled").Return(true) mm.On("TransferSubmitted", mock.Anything) + mom.On("RegisterHandler", mock.Anything, mock.Anything) ctx, cancel := context.WithCancel(context.Background()) a, err := NewAssetManager(ctx, mdi, mim, mdm, msa, mbm, mpm, map[string]tokens.Plugin{"magic-tokens": mti}, mm, mom) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() diff --git a/internal/assets/operations.go b/internal/assets/operations.go new file mode 100644 index 0000000000..adb6fd0ec8 --- /dev/null +++ b/internal/assets/operations.go @@ -0,0 +1,141 @@ +// Copyright © 2022 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 assets + +import ( + "context" + "fmt" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/internal/txcommon" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +type createPoolData struct { + Pool *fftypes.TokenPool +} + +type activatePoolData struct { + Pool *fftypes.TokenPool + BlockchainInfo fftypes.JSONObject +} + +type transferData struct { + Pool *fftypes.TokenPool + Transfer *fftypes.TokenTransfer +} + +func (am *assetManager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + switch op.Type { + case fftypes.OpTypeTokenCreatePool: + pool, err := txcommon.RetrieveTokenPoolCreateInputs(ctx, op) + if err != nil { + return nil, err + } + return opCreatePool(op, pool), nil + + case fftypes.OpTypeTokenActivatePool: + poolID, blockchainInfo, err := txcommon.RetrieveTokenPoolActivateInputs(ctx, op) + if err != nil { + return nil, err + } + pool, err := am.database.GetTokenPoolByID(ctx, poolID) + if err != nil { + return nil, err + } else if pool == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + } + return opActivatePool(op, pool, blockchainInfo), nil + + case fftypes.OpTypeTokenTransfer: + transfer, err := txcommon.RetrieveTokenTransferInputs(ctx, op) + if err != nil { + return nil, err + } + pool, err := am.database.GetTokenPoolByID(ctx, transfer.Pool) + if err != nil { + return nil, err + } else if pool == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + } + return opTransfer(op, pool, transfer), nil + + default: + return nil, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func (am *assetManager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) { + switch data := op.Data.(type) { + case createPoolData: + plugin, err := am.selectTokenPlugin(ctx, data.Pool.Connector) + if err != nil { + return false, err + } + return plugin.CreateTokenPool(ctx, op.ID, data.Pool) + + case activatePoolData: + plugin, err := am.selectTokenPlugin(ctx, data.Pool.Connector) + if err != nil { + return false, err + } + return plugin.ActivateTokenPool(ctx, op.ID, data.Pool, data.BlockchainInfo) + + case transferData: + plugin, err := am.selectTokenPlugin(ctx, data.Transfer.Connector) + if err != nil { + return false, err + } + switch data.Transfer.Type { + case fftypes.TokenTransferTypeMint: + return false, plugin.MintTokens(ctx, op.ID, data.Pool.ProtocolID, data.Transfer) + case fftypes.TokenTransferTypeTransfer: + return false, plugin.TransferTokens(ctx, op.ID, data.Pool.ProtocolID, data.Transfer) + case fftypes.TokenTransferTypeBurn: + return false, plugin.BurnTokens(ctx, op.ID, data.Pool.ProtocolID, data.Transfer) + default: + panic(fmt.Sprintf("unknown transfer type: %v", data.Transfer.Type)) + } + + default: + return false, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func opCreatePool(op *fftypes.Operation, pool *fftypes.TokenPool) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: createPoolData{Pool: pool}, + } +} + +func opActivatePool(op *fftypes.Operation, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: activatePoolData{Pool: pool, BlockchainInfo: blockchainInfo}, + } +} + +func opTransfer(op *fftypes.Operation, pool *fftypes.TokenPool, transfer *fftypes.TokenTransfer) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: transferData{Pool: pool, Transfer: transfer}, + } +} diff --git a/internal/assets/token_pool.go b/internal/assets/token_pool.go index c65964c4f7..ab51f8fc12 100644 --- a/internal/assets/token_pool.go +++ b/internal/assets/token_pool.go @@ -90,7 +90,7 @@ func (am *assetManager) createTokenPoolInternal(ctx context.Context, pool *fftyp return nil, err } - return pool, am.operations.StartOperation(ctx, op) + return pool, am.operations.RunOperation(ctx, opCreatePool(op, pool)) } func (am *assetManager) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) error { @@ -109,7 +109,7 @@ func (am *assetManager) ActivateTokenPool(ctx context.Context, pool *fftypes.Tok return err } - return am.operations.StartOperation(ctx, op) + return am.operations.RunOperation(ctx, opActivatePool(op, pool, blockchainInfo)) } func (am *assetManager) GetTokenPools(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) { diff --git a/internal/assets/token_pool_test.go b/internal/assets/token_pool_test.go index 116d4b1d55..887acf3944 100644 --- a/internal/assets/token_pool_test.go +++ b/internal/assets/token_pool_test.go @@ -63,8 +63,9 @@ func TestCreateTokenPoolUnknownConnectorSuccess(t *testing.T) { mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(createPoolData) + return op.Type == fftypes.OpTypeTokenCreatePool && data.Pool == pool })).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) @@ -210,8 +211,9 @@ func TestCreateTokenPoolFail(t *testing.T) { mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(createPoolData) + return op.Type == fftypes.OpTypeTokenCreatePool && data.Pool == pool })).Return(fmt.Errorf("pop")) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) @@ -293,8 +295,9 @@ func TestCreateTokenPoolSyncSuccess(t *testing.T) { mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(createPoolData) + return op.Type == fftypes.OpTypeTokenCreatePool && data.Pool == pool })).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) @@ -325,8 +328,9 @@ func TestCreateTokenPoolAsyncSuccess(t *testing.T) { mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(createPoolData) + return op.Type == fftypes.OpTypeTokenCreatePool && data.Pool == pool })).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) @@ -364,8 +368,9 @@ func TestCreateTokenPoolConfirm(t *testing.T) { send(context.Background()) }). Return(nil, nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenCreatePool && op.Input.GetString("name") == "testpool" && op.Input.GetString("connector") == "magic-tokens" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(createPoolData) + return op.Type == fftypes.OpTypeTokenCreatePool && data.Pool == pool })).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, true) @@ -397,9 +402,10 @@ func TestActivateTokenPool(t *testing.T) { mdi.On("InsertOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeTokenActivatePool })).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - assert.Equal(t, info, op.Input.GetObject("info")) - return op.Type == fftypes.OpTypeTokenActivatePool && op.Input.GetString("id") == pool.ID.String() + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(activatePoolData) + assert.Equal(t, info, data.BlockchainInfo) + return op.Type == fftypes.OpTypeTokenActivatePool && data.Pool == pool })).Return(nil) err := am.ActivateTokenPool(context.Background(), pool, info) @@ -462,9 +468,10 @@ func TestActivateTokenPoolFail(t *testing.T) { mdi.On("InsertOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeTokenActivatePool })).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - assert.Equal(t, info, op.Input.GetObject("info")) - return op.Type == fftypes.OpTypeTokenActivatePool && op.Input.GetString("id") == pool.ID.String() + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(activatePoolData) + assert.Equal(t, info, data.BlockchainInfo) + return op.Type == fftypes.OpTypeTokenActivatePool && data.Pool == pool })).Return(fmt.Errorf("pop")) err := am.ActivateTokenPool(context.Background(), pool, info) @@ -493,9 +500,10 @@ func TestActivateTokenPoolSyncSuccess(t *testing.T) { mdi.On("InsertOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeTokenActivatePool })).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - assert.Equal(t, info, op.Input.GetObject("info")) - return op.Type == fftypes.OpTypeTokenActivatePool && op.Input.GetString("id") == pool.ID.String() + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(activatePoolData) + assert.Equal(t, info, data.BlockchainInfo) + return op.Type == fftypes.OpTypeTokenActivatePool && data.Pool == pool })).Return(nil) err := am.ActivateTokenPool(context.Background(), pool, info) diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go index 2f08e16c4d..5f512eaf4d 100644 --- a/internal/assets/token_transfer.go +++ b/internal/assets/token_transfer.go @@ -228,8 +228,9 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er } var op *fftypes.Operation + var pool *fftypes.TokenPool err = s.mgr.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { - pool, err := s.mgr.GetTokenPoolByNameOrID(ctx, s.namespace, s.transfer.Pool) + pool, err = s.mgr.GetTokenPoolByNameOrID(ctx, s.namespace, s.transfer.Pool) if err != nil { return err } @@ -268,7 +269,7 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er return err } - return s.mgr.operations.StartOperation(ctx, op) + return s.mgr.operations.RunOperation(ctx, opTransfer(op, pool, &s.transfer.TokenTransfer)) } func (s *transferSender) buildTransferMessage(ctx context.Context, ns string, in *fftypes.MessageInOut) (sysmessaging.MessageSender, error) { diff --git a/internal/assets/token_transfer_test.go b/internal/assets/token_transfer_test.go index d3e091d4cc..b536a9242b 100644 --- a/internal/assets/token_transfer_test.go +++ b/internal/assets/token_transfer_test.go @@ -93,8 +93,9 @@ func TestMintTokensSuccess(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &mint.TokenTransfer })).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -128,8 +129,9 @@ func TestMintTokenUnknownConnectorSuccess(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &mint.TokenTransfer })).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -247,8 +249,9 @@ func TestMintTokenUnknownPoolSuccess(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(tokenPools[0], nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == tokenPools[0] && data.Transfer == &mint.TokenTransfer })).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -425,8 +428,9 @@ func TestMintTokensFail(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &mint.TokenTransfer })).Return(fmt.Errorf("pop")) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -499,8 +503,9 @@ func TestMintTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "mint" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &mint.TokenTransfer })).Return(fmt.Errorf("pop")) _, err := am.MintTokens(context.Background(), "ns1", mint, true) @@ -534,8 +539,9 @@ func TestBurnTokensSuccess(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "burn" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &burn.TokenTransfer })).Return(nil) _, err := am.BurnTokens(context.Background(), "ns1", burn, false) @@ -597,8 +603,9 @@ func TestBurnTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "burn" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &burn.TokenTransfer })).Return(nil) _, err := am.BurnTokens(context.Background(), "ns1", burn, true) @@ -635,8 +642,9 @@ func TestTransferTokensSuccess(t *testing.T) { mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &transfer.TokenTransfer })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) @@ -794,8 +802,9 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { mdi.On("UpsertMessage", context.Background(), mock.MatchedBy(func(msg *fftypes.Message) bool { return msg.State == fftypes.MessageStateStaged }), database.UpsertOptimizationNew).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &transfer.TokenTransfer })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) @@ -893,8 +902,9 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { mdi.On("UpsertMessage", context.Background(), mock.MatchedBy(func(msg *fftypes.Message) bool { return msg.State == fftypes.MessageStateStaged }), database.UpsertOptimizationNew).Return(nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &transfer.TokenTransfer })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) @@ -976,8 +986,9 @@ func TestTransferTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &transfer.TokenTransfer })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, true) @@ -1050,8 +1061,9 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { send(context.Background()) }). Return(&transfer.TokenTransfer, nil) - mom.On("StartOperation", context.Background(), mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenTransfer && op.Input.GetString("type") == "transfer" + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferData) + return op.Type == fftypes.OpTypeTokenTransfer && data.Pool == pool && data.Transfer == &transfer.TokenTransfer })).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, true) diff --git a/internal/operations/manager.go b/internal/operations/manager.go index 715638617b..f0522d061e 100644 --- a/internal/operations/manager.go +++ b/internal/operations/manager.go @@ -18,7 +18,6 @@ package operations import ( "context" - "fmt" "github.com/hyperledger/firefly/internal/i18n" "github.com/hyperledger/firefly/internal/txcommon" @@ -27,8 +26,15 @@ import ( "github.com/hyperledger/firefly/pkg/tokens" ) +type OperationHandler interface { + PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) + RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) +} + type Manager interface { - StartOperation(ctx context.Context, op *fftypes.Operation) error + RegisterHandler(handler OperationHandler, ops []fftypes.OpType) + PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) + RunOperation(ctx context.Context, op *fftypes.PreparedOperation) error } type operationsManager struct { @@ -36,6 +42,7 @@ type operationsManager struct { database database.Plugin txHelper txcommon.Helper tokens map[string]tokens.Plugin + handlers map[fftypes.OpType]OperationHandler } func NewOperationsManager(ctx context.Context, di database.Plugin, ti map[string]tokens.Plugin) (Manager, error) { @@ -47,105 +54,35 @@ func NewOperationsManager(ctx context.Context, di database.Plugin, ti map[string database: di, txHelper: txcommon.NewTransactionHelper(di), tokens: ti, + handlers: make(map[fftypes.OpType]OperationHandler), } return om, nil } -func (om *operationsManager) StartOperation(ctx context.Context, op *fftypes.Operation) error { - switch op.Type { - case fftypes.OpTypeTokenCreatePool: - pool, err := txcommon.RetrieveTokenPoolCreateInputs(ctx, op) - if err != nil { - return err - } - return om.createTokenPool(ctx, op.ID, pool) - - case fftypes.OpTypeTokenActivatePool: - poolID, blockchainInfo, err := txcommon.RetrieveTokenPoolActivateInputs(ctx, op) - if err != nil { - return err - } - pool, err := om.database.GetTokenPoolByID(ctx, poolID) - if err != nil { - return err - } else if pool == nil { - return i18n.NewError(ctx, i18n.Msg404NotFound) - } - return om.activateTokenPool(ctx, op.ID, pool, blockchainInfo) - - case fftypes.OpTypeTokenTransfer: - transfer, err := txcommon.RetrieveTokenTransferInputs(ctx, op) - if err != nil { - return err - } - pool, err := om.database.GetTokenPoolByID(ctx, transfer.Pool) - if err != nil { - return err - } else if pool == nil { - return i18n.NewError(ctx, i18n.Msg404NotFound) - } - return om.transferTokens(ctx, op.ID, pool.ProtocolID, transfer) - - default: - return i18n.NewError(ctx, i18n.MsgOperationNotSupported) - } -} - -func (om *operationsManager) selectTokenPlugin(ctx context.Context, name string) (tokens.Plugin, error) { - for pluginName, plugin := range om.tokens { - if pluginName == name { - return plugin, nil - } +func (om *operationsManager) RegisterHandler(handler OperationHandler, ops []fftypes.OpType) { + for _, opType := range ops { + om.handlers[opType] = handler } - return nil, i18n.NewError(ctx, i18n.MsgUnknownTokensPlugin, name) } -func (om *operationsManager) createTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool) error { - plugin, err := om.selectTokenPlugin(ctx, pool.Connector) - if err != nil { - return err - } - if complete, err := plugin.CreateTokenPool(ctx, opID, pool); err != nil { - om.txHelper.WriteOperationFailure(ctx, opID, err) - return err - } else if complete { - om.txHelper.WriteOperationSuccess(ctx, opID, nil) +func (om *operationsManager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + handler, ok := om.handlers[op.Type] + if !ok { + return nil, i18n.NewError(ctx, i18n.MsgOperationNotSupported) } - return nil + return handler.PrepareOperation(ctx, op) } -func (om *operationsManager) activateTokenPool(ctx context.Context, opID *fftypes.UUID, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) error { - plugin, err := om.selectTokenPlugin(ctx, pool.Connector) - if err != nil { - return err +func (om *operationsManager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) error { + handler, ok := om.handlers[op.Type] + if !ok { + return i18n.NewError(ctx, i18n.MsgOperationNotSupported) } - if complete, err := plugin.ActivateTokenPool(ctx, opID, pool, blockchainInfo); err != nil { - om.txHelper.WriteOperationFailure(ctx, opID, err) + if complete, err := handler.RunOperation(ctx, op); err != nil { + om.txHelper.WriteOperationFailure(ctx, op.ID, err) return err } else if complete { - om.txHelper.WriteOperationSuccess(ctx, opID, nil) - } - return nil -} - -func (om *operationsManager) transferTokens(ctx context.Context, opID *fftypes.UUID, poolProtocolID string, transfer *fftypes.TokenTransfer) error { - plugin, err := om.selectTokenPlugin(ctx, transfer.Connector) - if err != nil { - return err - } - switch transfer.Type { - case fftypes.TokenTransferTypeMint: - err = plugin.MintTokens(ctx, opID, poolProtocolID, transfer) - case fftypes.TokenTransferTypeTransfer: - err = plugin.TransferTokens(ctx, opID, poolProtocolID, transfer) - case fftypes.TokenTransferTypeBurn: - err = plugin.BurnTokens(ctx, opID, poolProtocolID, transfer) - default: - panic(fmt.Sprintf("unknown transfer type: %v", transfer.Type)) - } - if err != nil { - om.txHelper.WriteOperationFailure(ctx, opID, err) - return err + om.txHelper.WriteOperationSuccess(ctx, op.ID, nil) } return nil } diff --git a/internal/operations/manager_test.go b/internal/operations/manager_test.go index c496796270..d5580344b1 100644 --- a/internal/operations/manager_test.go +++ b/internal/operations/manager_test.go @@ -37,12 +37,12 @@ func newTestOperations(t *testing.T) (*operationsManager, func()) { return om.(*operationsManager), cancel } -func TestStartOperationNotSupported(t *testing.T) { +func TestPrepareOperationNotSupported(t *testing.T) { om, cancel := newTestOperations(t) defer cancel() op := &fftypes.Operation{} - err := om.StartOperation(context.Background(), op) + _, err := om.PrepareOperation(context.Background(), op) assert.Regexp(t, "FF10346", err) } diff --git a/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index 1301f8a26d..1cbd37c0ff 100644 --- a/mocks/assetmocks/manager.go +++ b/mocks/assetmocks/manager.go @@ -369,6 +369,50 @@ func (_m *Manager) NewTransfer(ns string, transfer *fftypes.TokenTransferInput) return r0 } +// PrepareOperation provides a mock function with given fields: ctx, op +func (_m *Manager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + ret := _m.Called(ctx, op) + + var r0 *fftypes.PreparedOperation + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) *fftypes.PreparedOperation); ok { + r0 = rf(ctx, op) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.PreparedOperation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.Operation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RunOperation provides a mock function with given fields: ctx, op +func (_m *Manager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (bool, error) { + ret := _m.Called(ctx, op) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.PreparedOperation) bool); ok { + r0 = rf(ctx, op) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.PreparedOperation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // TransferTokens provides a mock function with given fields: ctx, ns, transfer, waitConfirm func (_m *Manager) TransferTokens(ctx context.Context, ns string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (*fftypes.TokenTransfer, error) { ret := _m.Called(ctx, ns, transfer, waitConfirm) diff --git a/mocks/operationmocks/manager.go b/mocks/operationmocks/manager.go index 5f53e8c005..3339e2bb5d 100644 --- a/mocks/operationmocks/manager.go +++ b/mocks/operationmocks/manager.go @@ -7,6 +7,8 @@ import ( fftypes "github.com/hyperledger/firefly/pkg/fftypes" mock "github.com/stretchr/testify/mock" + + operations "github.com/hyperledger/firefly/internal/operations" ) // Manager is an autogenerated mock type for the Manager type @@ -14,12 +16,40 @@ type Manager struct { mock.Mock } -// StartOperation provides a mock function with given fields: ctx, op -func (_m *Manager) StartOperation(ctx context.Context, op *fftypes.Operation) error { +// PrepareOperation provides a mock function with given fields: ctx, op +func (_m *Manager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + ret := _m.Called(ctx, op) + + var r0 *fftypes.PreparedOperation + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) *fftypes.PreparedOperation); ok { + r0 = rf(ctx, op) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.PreparedOperation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.Operation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegisterHandler provides a mock function with given fields: handler, ops +func (_m *Manager) RegisterHandler(handler operations.OperationHandler, ops []fftypes.FFEnum) { + _m.Called(handler, ops) +} + +// RunOperation provides a mock function with given fields: ctx, op +func (_m *Manager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) error { ret := _m.Called(ctx, op) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.PreparedOperation) error); ok { r0 = rf(ctx, op) } else { r0 = ret.Error(0) diff --git a/pkg/fftypes/operation.go b/pkg/fftypes/operation.go index 4fa6354d68..5f6971c5c2 100644 --- a/pkg/fftypes/operation.go +++ b/pkg/fftypes/operation.go @@ -46,7 +46,7 @@ type OpStatus string const ( // OpStatusPending indicates the operation has been submitted, but is not yet confirmed as successful or failed OpStatusPending OpStatus = "Pending" - // OpStatusSucceeded the infrastructure runtime has returned success for the operation. + // OpStatusSucceeded the infrastructure runtime has returned success for the operation OpStatusSucceeded OpStatus = "Succeeded" // OpStatusFailed happens when an error is reported by the infrastructure runtime OpStatusFailed OpStatus = "Failed" @@ -85,3 +85,13 @@ type Operation struct { Created *FFTime `json:"created,omitempty"` Updated *FFTime `json:"updated,omitempty"` } + +// PreparedOperation is an operation that has gathered all the raw data ready to send to a plugin +// It is never stored, but it should always be possible for the owning Manager to generate a +// PreparedOperation from an Operation. Data is defined by the Manager, but should be JSON-serializable +// to support inspection and debugging. +type PreparedOperation struct { + ID *UUID `json:"id"` + Type OpType `json:"type" ffenum:"optype"` + Data interface{} `json:"data"` +} From 05605e5abcb6248a9d73172f0b10f2c63103b386 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 16 Feb 2022 12:46:19 -0500 Subject: [PATCH 03/10] Add handling for all remaining operations to Operations Manager Signed-off-by: Andrew Richardson --- internal/assets/manager.go | 8 +- internal/assets/operations.go | 10 +- internal/batchpin/batchpin.go | 27 ++-- internal/batchpin/batchpin_test.go | 4 +- internal/batchpin/operations.go | 101 ++++++++++++ internal/broadcast/manager.go | 59 +++---- internal/broadcast/manager_test.go | 86 +--------- internal/broadcast/operations.go | 92 +++++++++++ internal/contracts/manager.go | 53 +++--- internal/contracts/manager_test.go | 12 +- internal/contracts/operations.go | 79 +++++++++ internal/orchestrator/orchestrator.go | 12 +- internal/privatemessaging/operations.go | 152 ++++++++++++++++++ internal/privatemessaging/privatemessaging.go | 41 ++--- .../privatemessaging/privatemessaging_test.go | 6 +- mocks/batchpinmocks/submitter.go | 44 +++++ mocks/broadcastmocks/manager.go | 44 +++++ mocks/contractmocks/manager.go | 44 +++++ mocks/privatemessagingmocks/manager.go | 44 +++++ 19 files changed, 726 insertions(+), 192 deletions(-) create mode 100644 internal/batchpin/operations.go create mode 100644 internal/broadcast/operations.go create mode 100644 internal/contracts/operations.go create mode 100644 internal/privatemessaging/operations.go diff --git a/internal/assets/manager.go b/internal/assets/manager.go index 3869448736..e0f91d0713 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -74,8 +74,8 @@ type assetManager struct { operations operations.Manager } -func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manager, dm data.Manager, sa syncasync.Bridge, bm broadcast.Manager, pm privatemessaging.Manager, ti map[string]tokens.Plugin, mm metrics.Manager, ops operations.Manager) (Manager, error) { - if di == nil || im == nil || sa == nil || bm == nil || pm == nil || ti == nil || mm == nil || ops == nil { +func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manager, dm data.Manager, sa syncasync.Bridge, bm broadcast.Manager, pm privatemessaging.Manager, ti map[string]tokens.Plugin, mm metrics.Manager, om operations.Manager) (Manager, error) { + if di == nil || im == nil || sa == nil || bm == nil || pm == nil || ti == nil || mm == nil || om == nil { return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) } am := &assetManager{ @@ -89,9 +89,9 @@ func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manage messaging: pm, tokens: ti, metrics: mm, - operations: ops, + operations: om, } - ops.RegisterHandler(am, []fftypes.OpType{ + om.RegisterHandler(am, []fftypes.OpType{ fftypes.OpTypeTokenCreatePool, fftypes.OpTypeTokenActivatePool, fftypes.OpTypeTokenTransfer, diff --git a/internal/assets/operations.go b/internal/assets/operations.go index adb6fd0ec8..bf8e84075d 100644 --- a/internal/assets/operations.go +++ b/internal/assets/operations.go @@ -26,17 +26,17 @@ import ( ) type createPoolData struct { - Pool *fftypes.TokenPool + Pool *fftypes.TokenPool `json:"pool"` } type activatePoolData struct { - Pool *fftypes.TokenPool - BlockchainInfo fftypes.JSONObject + Pool *fftypes.TokenPool `json:"pool"` + BlockchainInfo fftypes.JSONObject `json:"blockchainInfo"` } type transferData struct { - Pool *fftypes.TokenPool - Transfer *fftypes.TokenTransfer + Pool *fftypes.TokenPool `json:"pool"` + Transfer *fftypes.TokenTransfer `json:"transfer"` } func (am *assetManager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { diff --git a/internal/batchpin/batchpin.go b/internal/batchpin/batchpin.go index 6334abb127..e14142892a 100644 --- a/internal/batchpin/batchpin.go +++ b/internal/batchpin/batchpin.go @@ -21,6 +21,7 @@ import ( "github.com/hyperledger/firefly/internal/identity" "github.com/hyperledger/firefly/internal/metrics" + "github.com/hyperledger/firefly/internal/operations" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" @@ -28,6 +29,10 @@ import ( type Submitter interface { SubmitPinnedBatch(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error + + // From operations.OperationHandler + PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) + RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) } type batchPinSubmitter struct { @@ -35,25 +40,31 @@ type batchPinSubmitter struct { identity identity.Manager blockchain blockchain.Plugin metrics metrics.Manager + operations operations.Manager } -func NewBatchPinSubmitter(di database.Plugin, im identity.Manager, bi blockchain.Plugin, mm metrics.Manager) Submitter { - return &batchPinSubmitter{ +func NewBatchPinSubmitter(di database.Plugin, im identity.Manager, bi blockchain.Plugin, mm metrics.Manager, om operations.Manager) Submitter { + bp := &batchPinSubmitter{ database: di, identity: im, blockchain: bi, metrics: mm, + operations: om, } + om.RegisterHandler(bp, []fftypes.OpType{ + fftypes.OpTypeBlockchainBatchPin, + }) + return bp } func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error { - // The pending blockchain transaction op := fftypes.NewOperation( bp.blockchain, batch.Namespace, batch.Payload.TX.ID, fftypes.OpTypeBlockchainBatchPin) + addBatchPinInputs(op, batch.ID, contexts) if err := bp.database.InsertOperation(ctx, op); err != nil { return err } @@ -61,13 +72,5 @@ func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *fftyp if bp.metrics.IsMetricsEnabled() { bp.metrics.CountBatchPin() } - // Write the batch pin to the blockchain - return bp.blockchain.SubmitBatchPin(ctx, op.ID, nil /* TODO: ledger selection */, batch.Key, &blockchain.BatchPin{ - Namespace: batch.Namespace, - TransactionID: batch.Payload.TX.ID, - BatchID: batch.ID, - BatchHash: batch.Hash, - BatchPayloadRef: batch.PayloadRef, - Contexts: contexts, - }) + return bp.operations.RunOperation(ctx, opBatchPin(op, batch, contexts)) } diff --git a/internal/batchpin/batchpin_test.go b/internal/batchpin/batchpin_test.go index b4ee1667a7..2b0ff17161 100644 --- a/internal/batchpin/batchpin_test.go +++ b/internal/batchpin/batchpin_test.go @@ -26,6 +26,7 @@ import ( "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/mocks/metricsmocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -38,12 +39,13 @@ func newTestBatchPinSubmitter(t *testing.T, enableMetrics bool) *batchPinSubmitt mim := &identitymanagermocks.Manager{} mbi := &blockchainmocks.Plugin{} mmi := &metricsmocks.Manager{} + mom := &operationmocks.Manager{} mmi.On("IsMetricsEnabled").Return(enableMetrics) if enableMetrics { mmi.On("CountBatchPin").Return() } mbi.On("Name").Return("ut").Maybe() - bps := NewBatchPinSubmitter(mdi, mim, mbi, mmi).(*batchPinSubmitter) + bps := NewBatchPinSubmitter(mdi, mim, mbi, mmi, mom).(*batchPinSubmitter) return bps } diff --git a/internal/batchpin/operations.go b/internal/batchpin/operations.go new file mode 100644 index 0000000000..fa39d2f872 --- /dev/null +++ b/internal/batchpin/operations.go @@ -0,0 +1,101 @@ +// Copyright © 2022 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 batchpin + +import ( + "context" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/pkg/blockchain" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +type batchPinData struct { + Batch *fftypes.Batch `json:"batch"` + Contexts []*fftypes.Bytes32 `json:"contexts"` +} + +func addBatchPinInputs(op *fftypes.Operation, batchID *fftypes.UUID, contexts []*fftypes.Bytes32) { + contextStr := make([]string, len(contexts)) + for i, c := range contexts { + contextStr[i] = c.String() + } + op.Input = fftypes.JSONObject{ + "batch": batchID.String(), + "contexts": contextStr, + } +} + +func retrieveBatchPinInputs(ctx context.Context, op *fftypes.Operation) (batchID *fftypes.UUID, contexts []*fftypes.Bytes32, err error) { + batchID, err = fftypes.ParseUUID(ctx, op.Input.GetString("batch")) + if err != nil { + return nil, nil, err + } + contextStr := op.Input.GetStringArray("contexts") + contexts = make([]*fftypes.Bytes32, len(contextStr)) + for i, c := range contextStr { + contexts[i], err = fftypes.ParseBytes32(ctx, c) + if err != nil { + return nil, nil, err + } + } + return batchID, contexts, nil +} + +func (bp *batchPinSubmitter) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + switch op.Type { + case fftypes.OpTypeBlockchainBatchPin: + batchID, contexts, err := retrieveBatchPinInputs(ctx, op) + if err != nil { + return nil, err + } + batch, err := bp.database.GetBatchByID(ctx, batchID) + if err != nil { + return nil, err + } + return opBatchPin(op, batch, contexts), nil + + default: + return nil, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func (bp *batchPinSubmitter) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) { + switch data := op.Data.(type) { + case batchPinData: + batch := data.Batch + return false, bp.blockchain.SubmitBatchPin(ctx, op.ID, nil /* TODO: ledger selection */, batch.Key, &blockchain.BatchPin{ + Namespace: batch.Namespace, + TransactionID: batch.Payload.TX.ID, + BatchID: batch.ID, + BatchHash: batch.Hash, + BatchPayloadRef: batch.PayloadRef, + Contexts: data.Contexts, + }) + + default: + return false, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func opBatchPin(op *fftypes.Operation, batch *fftypes.Batch, contexts []*fftypes.Bytes32) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: batchPinData{Batch: batch, Contexts: contexts}, + } +} diff --git a/internal/broadcast/manager.go b/internal/broadcast/manager.go index 3f580553e9..b9b4c8792e 100644 --- a/internal/broadcast/manager.go +++ b/internal/broadcast/manager.go @@ -17,9 +17,7 @@ package broadcast import ( - "bytes" "context" - "encoding/json" "github.com/hyperledger/firefly/internal/batch" "github.com/hyperledger/firefly/internal/batchpin" @@ -29,6 +27,7 @@ import ( "github.com/hyperledger/firefly/internal/identity" "github.com/hyperledger/firefly/internal/log" "github.com/hyperledger/firefly/internal/metrics" + "github.com/hyperledger/firefly/internal/operations" "github.com/hyperledger/firefly/internal/syncasync" "github.com/hyperledger/firefly/internal/sysmessaging" "github.com/hyperledger/firefly/pkg/blockchain" @@ -51,6 +50,10 @@ type Manager interface { BroadcastTokenPool(ctx context.Context, ns string, pool *fftypes.TokenPoolAnnouncement, waitConfirm bool) (msg *fftypes.Message, err error) Start() error WaitStop() + + // From operations.OperationHandler + PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) + RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) } type broadcastManager struct { @@ -66,10 +69,11 @@ type broadcastManager struct { batchpin batchpin.Submitter maxBatchPayloadLength int64 metrics metrics.Manager + operations operations.Manager } -func NewBroadcastManager(ctx context.Context, di database.Plugin, im identity.Manager, dm data.Manager, bi blockchain.Plugin, dx dataexchange.Plugin, pi publicstorage.Plugin, ba batch.Manager, sa syncasync.Bridge, bp batchpin.Submitter, mm metrics.Manager) (Manager, error) { - if di == nil || im == nil || dm == nil || bi == nil || dx == nil || pi == nil || ba == nil { +func NewBroadcastManager(ctx context.Context, di database.Plugin, im identity.Manager, dm data.Manager, bi blockchain.Plugin, dx dataexchange.Plugin, pi publicstorage.Plugin, ba batch.Manager, sa syncasync.Bridge, bp batchpin.Submitter, mm metrics.Manager, om operations.Manager) (Manager, error) { + if di == nil || im == nil || dm == nil || bi == nil || dx == nil || pi == nil || ba == nil || mm == nil || om == nil { return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) } bm := &broadcastManager{ @@ -85,13 +89,16 @@ func NewBroadcastManager(ctx context.Context, di database.Plugin, im identity.Ma batchpin: bp, maxBatchPayloadLength: config.GetByteSize(config.BroadcastBatchPayloadLimit), metrics: mm, + operations: om, } + bo := batch.DispatcherOptions{ BatchMaxSize: config.GetUint(config.BroadcastBatchSize), BatchMaxBytes: bm.maxBatchPayloadLength, BatchTimeout: config.GetDuration(config.BroadcastBatchTimeout), DisposeTimeout: config.GetDuration(config.BroadcastBatchAgentTimeout), } + ba.RegisterDispatcher(broadcastDispatcherName, fftypes.TransactionTypeBatchPin, []fftypes.MessageType{ @@ -99,50 +106,28 @@ func NewBroadcastManager(ctx context.Context, di database.Plugin, im identity.Ma fftypes.MessageTypeDefinition, fftypes.MessageTypeTransferBroadcast, }, bm.dispatchBatch, bo) - return bm, nil -} - -func (bm *broadcastManager) dispatchBatch(ctx context.Context, batch *fftypes.Batch, pins []*fftypes.Bytes32) error { - - // Serialize the full payload, which has already been sealed for us by the BatchManager - payload, err := json.Marshal(batch) - if err != nil { - return i18n.WrapError(ctx, err, i18n.MsgSerializationFailed) - } - - // Write it to IPFS to get a payload reference - // The payload ref will be persisted back to the batch, as well as being used in the TX - batch.PayloadRef, err = bm.publicstorage.PublishData(ctx, bytes.NewReader(payload)) - if err != nil { - return err - } - return bm.database.RunAsGroup(ctx, func(ctx context.Context) error { - return bm.submitTXAndUpdateDB(ctx, batch, pins) + om.RegisterHandler(bm, []fftypes.OpType{ + fftypes.OpTypePublicStorageBatchBroadcast, }) -} - -func (bm *broadcastManager) submitTXAndUpdateDB(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error { - // Update the batch to store the payloadRef - err := bm.database.UpdateBatch(ctx, batch.ID, database.BatchQueryFactory.NewUpdate(ctx).Set("payloadref", batch.PayloadRef)) - if err != nil { - return err - } + return bm, nil +} - // The completed PublicStorage upload +func (bm *broadcastManager) dispatchBatch(ctx context.Context, batch *fftypes.Batch, pins []*fftypes.Bytes32) error { op := fftypes.NewOperation( bm.publicstorage, batch.Namespace, batch.Payload.TX.ID, fftypes.OpTypePublicStorageBatchBroadcast) - op.Status = fftypes.OpStatusSucceeded // Note we performed the action synchronously above - err = bm.database.InsertOperation(ctx, op) - if err != nil { + addBatchBroadcastInputs(op, batch.ID) + if err := bm.database.InsertOperation(ctx, op); err != nil { return err } - - return bm.batchpin.SubmitPinnedBatch(ctx, batch, contexts) + if err := bm.operations.RunOperation(ctx, opBatchBroadcast(op, batch)); err != nil { + return err + } + return bm.batchpin.SubmitPinnedBatch(ctx, batch, pins) } func (bm *broadcastManager) publishBlobs(ctx context.Context, dataToPublish []*fftypes.DataAndBlob) error { diff --git a/internal/broadcast/manager_test.go b/internal/broadcast/manager_test.go index 48291c6e1d..a1444d4c37 100644 --- a/internal/broadcast/manager_test.go +++ b/internal/broadcast/manager_test.go @@ -33,6 +33,7 @@ import ( "github.com/hyperledger/firefly/mocks/datamocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/mocks/metricsmocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/mocks/publicstoragemocks" "github.com/hyperledger/firefly/mocks/syncasyncmocks" "github.com/hyperledger/firefly/pkg/database" @@ -53,6 +54,7 @@ func newTestBroadcastCommon(t *testing.T, metricsEnabled bool) (*broadcastManage msa := &syncasyncmocks.Bridge{} mbp := &batchpinmocks.Submitter{} mmi := &metricsmocks.Manager{} + mom := &operationmocks.Manager{} mmi.On("IsMetricsEnabled").Return(metricsEnabled) mbi.On("Name").Return("ut_blockchain").Maybe() mpi.On("Name").Return("ut_publicstorage").Maybe() @@ -73,7 +75,7 @@ func newTestBroadcastCommon(t *testing.T, metricsEnabled bool) (*broadcastManage } ctx, cancel := context.WithCancel(context.Background()) - b, err := NewBroadcastManager(ctx, mdi, mim, mdm, mbi, mdx, mpi, mba, msa, mbp, mmi) + b, err := NewBroadcastManager(ctx, mdi, mim, mdm, mbi, mdx, mpi, mba, msa, mbp, mmi, mom) assert.NoError(t, err) return b.(*broadcastManager), cancel } @@ -90,7 +92,7 @@ func newTestBroadcastWithMetrics(t *testing.T) (*broadcastManager, func()) { } func TestInitFail(t *testing.T) { - _, err := NewBroadcastManager(context.Background(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + _, err := NewBroadcastManager(context.Background(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) assert.Regexp(t, "FF10128", err) } @@ -194,86 +196,6 @@ func TestDispatchBatchSubmitBroadcastFail(t *testing.T) { assert.EqualError(t, err, "pop") } -func TestSubmitTXAndUpdateDBUpdateBatchFail(t *testing.T) { - bm, cancel := newTestBroadcast(t) - defer cancel() - - mdi := bm.database.(*databasemocks.Plugin) - mdi.On("UpsertTransaction", mock.Anything, mock.Anything, false).Return(nil) - mdi.On("UpdateBatch", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) - bm.blockchain.(*blockchainmocks.Plugin).On("SubmitBatchPin", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", fmt.Errorf("pop")) - - err := bm.submitTXAndUpdateDB(context.Background(), &fftypes.Batch{Identity: fftypes.Identity{Author: "org1", Key: "0x12345"}}, []*fftypes.Bytes32{fftypes.NewRandB32()}) - assert.Regexp(t, "pop", err) -} - -func TestSubmitTXAndUpdateDBAddOp1Fail(t *testing.T) { - bm, cancel := newTestBroadcast(t) - defer cancel() - - mdi := bm.database.(*databasemocks.Plugin) - mbi := bm.blockchain.(*blockchainmocks.Plugin) - mdi.On("UpsertTransaction", mock.Anything, mock.Anything, false).Return(nil) - mdi.On("UpdateBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) - mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) - mbi.On("SubmitBatchPin", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("txid", nil) - mbi.On("Name").Return("unittest") - - batch := &fftypes.Batch{ - Identity: fftypes.Identity{Author: "org1", Key: "0x12345"}, - Payload: fftypes.BatchPayload{ - Messages: []*fftypes.Message{ - {Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - }}, - }, - }, - } - - err := bm.submitTXAndUpdateDB(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) - assert.Regexp(t, "pop", err) -} - -func TestSubmitTXAndUpdateDBSucceed(t *testing.T) { - bm, cancel := newTestBroadcast(t) - defer cancel() - - mdi := bm.database.(*databasemocks.Plugin) - mbi := bm.blockchain.(*blockchainmocks.Plugin) - mbp := bm.batchpin.(*batchpinmocks.Submitter) - mdi.On("UpsertTransaction", mock.Anything, mock.Anything, false).Return(nil) - mdi.On("UpdateBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) - mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) - mbi.On("SubmitBatchPin", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - mbp.On("SubmitPinnedBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) - - msgID := fftypes.NewUUID() - batch := &fftypes.Batch{ - Identity: fftypes.Identity{Author: "org1", Key: "0x12345"}, - Payload: fftypes.BatchPayload{ - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeBatchPin, - ID: fftypes.NewUUID(), - }, - Messages: []*fftypes.Message{ - {Header: fftypes.MessageHeader{ - ID: msgID, - }}, - }, - }, - PayloadRef: "ipfs_id", - } - - err := bm.submitTXAndUpdateDB(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) - assert.NoError(t, err) - - op := mdi.Calls[1].Arguments[1].(*fftypes.Operation) - assert.Equal(t, *batch.Payload.TX.ID, *op.Transaction) - assert.Equal(t, "ut_publicstorage", op.Plugin) - assert.Equal(t, fftypes.OpTypePublicStorageBatchBroadcast, op.Type) - -} - func TestPublishBlobsUpdateDataFail(t *testing.T) { bm, cancel := newTestBroadcast(t) defer cancel() diff --git a/internal/broadcast/operations.go b/internal/broadcast/operations.go new file mode 100644 index 0000000000..8b9c718b9c --- /dev/null +++ b/internal/broadcast/operations.go @@ -0,0 +1,92 @@ +// Copyright © 2022 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 broadcast + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +type batchBroadcastData struct { + Batch *fftypes.Batch `json:"batch"` +} + +func addBatchBroadcastInputs(op *fftypes.Operation, batchID *fftypes.UUID) { + op.Input = fftypes.JSONObject{ + "id": batchID.String(), + } +} + +func retrieveBatchBroadcastInputs(ctx context.Context, op *fftypes.Operation) (batchID *fftypes.UUID, err error) { + return fftypes.ParseUUID(ctx, op.Input.GetString("id")) +} + +func (bm *broadcastManager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + switch op.Type { + case fftypes.OpTypePublicStorageBatchBroadcast: + id, err := retrieveBatchBroadcastInputs(ctx, op) + if err != nil { + return nil, err + } + batch, err := bm.database.GetBatchByID(ctx, id) + if err != nil { + return nil, err + } + return opBatchBroadcast(op, batch), nil + + default: + return nil, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func (bm *broadcastManager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) { + switch data := op.Data.(type) { + case batchBroadcastData: + // Serialize the full payload, which has already been sealed for us by the BatchManager + payload, err := json.Marshal(data.Batch) + if err != nil { + return false, i18n.WrapError(ctx, err, i18n.MsgSerializationFailed) + } + + // Write it to IPFS to get a payload reference + payloadRef, err := bm.publicstorage.PublishData(ctx, bytes.NewReader(payload)) + if err != nil { + return false, err + } + + // Update the batch to store the payloadRef + data.Batch.PayloadRef = payloadRef + update := database.BatchQueryFactory.NewUpdate(ctx).Set("payloadref", payloadRef) + return true, bm.database.UpdateBatch(ctx, data.Batch.ID, update) + + default: + return false, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func opBatchBroadcast(op *fftypes.Operation, batch *fftypes.Batch) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: batchBroadcastData{Batch: batch}, + } +} diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index 266edd2ac6..517890c702 100644 --- a/internal/contracts/manager.go +++ b/internal/contracts/manager.go @@ -24,6 +24,7 @@ import ( "github.com/hyperledger/firefly/internal/broadcast" "github.com/hyperledger/firefly/internal/i18n" "github.com/hyperledger/firefly/internal/identity" + "github.com/hyperledger/firefly/internal/operations" "github.com/hyperledger/firefly/internal/txcommon" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/database" @@ -53,6 +54,10 @@ type Manager interface { GetContractSubscriptionByNameOrID(ctx context.Context, ns, nameOrID string) (*fftypes.ContractSubscription, error) GetContractSubscriptions(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.ContractSubscription, *database.FilterResult, error) DeleteContractSubscriptionByNameOrID(ctx context.Context, ns, nameOrID string) error + + // From operations.OperationHandler + PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) + RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) } type contractManager struct { @@ -63,25 +68,34 @@ type contractManager struct { identity identity.Manager blockchain blockchain.Plugin ffiParamValidator fftypes.FFIParamValidator + operations operations.Manager } -func NewContractManager(ctx context.Context, database database.Plugin, publicStorage publicstorage.Plugin, broadcast broadcast.Manager, identity identity.Manager, blockchain blockchain.Plugin) (Manager, error) { - if database == nil || publicStorage == nil || broadcast == nil || identity == nil || blockchain == nil { +func NewContractManager(ctx context.Context, di database.Plugin, ps publicstorage.Plugin, bm broadcast.Manager, im identity.Manager, bi blockchain.Plugin, om operations.Manager) (Manager, error) { + if di == nil || ps == nil || bm == nil || im == nil || bi == nil || om == nil { return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) } - v, err := blockchain.GetFFIParamValidator(ctx) + v, err := bi.GetFFIParamValidator(ctx) if err != nil { return nil, i18n.WrapError(ctx, err, i18n.MsgPluginInitializationFailed) } - return &contractManager{ - database: database, - txHelper: txcommon.NewTransactionHelper(database), - publicStorage: publicStorage, - broadcast: broadcast, - identity: identity, - blockchain: blockchain, + + cm := &contractManager{ + database: di, + txHelper: txcommon.NewTransactionHelper(di), + publicStorage: ps, + broadcast: bm, + identity: im, + blockchain: bi, ffiParamValidator: v, - }, nil + operations: om, + } + + om.RegisterHandler(cm, []fftypes.OpType{ + fftypes.OpTypeBlockchainInvoke, + }) + + return cm, nil } func (cm *contractManager) newFFISchemaCompiler() *jsonschema.Compiler { @@ -160,7 +174,7 @@ func (cm *contractManager) GetFFIs(ctx context.Context, ns string, filter databa return cm.database.GetFFIs(ctx, ns, filter) } -func (cm *contractManager) writeInvokeTransaction(ctx context.Context, ns string, input fftypes.JSONObject) (*fftypes.Operation, error) { +func (cm *contractManager) writeInvokeTransaction(ctx context.Context, ns string, req *fftypes.ContractCallRequest) (*fftypes.Operation, error) { txid, err := cm.txHelper.SubmitNewTransaction(ctx, ns, fftypes.TransactionTypeContractInvoke) if err != nil { return nil, err @@ -171,7 +185,9 @@ func (cm *contractManager) writeInvokeTransaction(ctx context.Context, ns string ns, txid, fftypes.OpTypeBlockchainInvoke) - op.Input = input + if err := addBlockchainInvokeInputs(op, req); err != nil { + return nil, err + } return op, cm.database.InsertOperation(ctx, op) } @@ -190,7 +206,7 @@ func (cm *contractManager) InvokeContract(ctx context.Context, ns string, req *f return err } if req.Type == fftypes.CallTypeInvoke { - op, err = cm.writeInvokeTransaction(ctx, ns, req.Input) + op, err = cm.writeInvokeTransaction(ctx, ns, req) if err != nil { return err } @@ -203,18 +219,13 @@ func (cm *contractManager) InvokeContract(ctx context.Context, ns string, req *f switch req.Type { case fftypes.CallTypeInvoke: - err = cm.blockchain.InvokeContract(ctx, op.ID, req.Key, req.Location, req.Method, req.Input) res = &fftypes.ContractCallResponse{ID: op.ID} + return res, cm.operations.RunOperation(ctx, opBlockchainInvoke(op, req)) case fftypes.CallTypeQuery: - res, err = cm.blockchain.QueryContract(ctx, req.Location, req.Method, req.Input) + return cm.blockchain.QueryContract(ctx, req.Location, req.Method, req.Input) default: panic(fmt.Sprintf("unknown call type: %s", req.Type)) } - - if op != nil && err != nil { - cm.txHelper.WriteOperationFailure(ctx, op.ID, err) - } - return res, err } func (cm *contractManager) InvokeContractAPI(ctx context.Context, ns, apiName, methodPath string, req *fftypes.ContractCallRequest) (interface{}, error) { diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index 3886d4f618..308d09f5e3 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -26,6 +26,7 @@ import ( "github.com/hyperledger/firefly/mocks/broadcastmocks" "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/mocks/publicstoragemocks" "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/database" @@ -41,6 +42,7 @@ func newTestContractManager() *contractManager { mbm := &broadcastmocks.Manager{} mim := &identitymanagermocks.Manager{} mbi := &blockchainmocks.Plugin{} + mom := &operationmocks.Manager{} mbi.On("GetFFIParamValidator", mock.Anything).Return(nil, nil) mbi.On("Name").Return("mockblockchain").Maybe() @@ -51,13 +53,13 @@ func newTestContractManager() *contractManager { a[1].(func(context.Context) error)(a[0].(context.Context)), } } - cm, _ := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi) + cm, _ := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi, mom) cm.(*contractManager).txHelper = &txcommonmocks.Helper{} return cm.(*contractManager) } func TestNewContractManagerFail(t *testing.T) { - _, err := NewContractManager(context.Background(), nil, nil, nil, nil, nil) + _, err := NewContractManager(context.Background(), nil, nil, nil, nil, nil, nil) assert.Regexp(t, "FF10128", err) } @@ -67,8 +69,9 @@ func TestNewContractManagerFFISchemaLoaderFail(t *testing.T) { mbm := &broadcastmocks.Manager{} mim := &identitymanagermocks.Manager{} mbi := &blockchainmocks.Plugin{} + mom := &operationmocks.Manager{} mbi.On("GetFFIParamValidator", mock.Anything).Return(nil, fmt.Errorf("pop")) - _, err := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi) + _, err := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi, mom) assert.Regexp(t, "pop", err) } @@ -78,8 +81,9 @@ func TestNewContractManagerFFISchemaLoader(t *testing.T) { mbm := &broadcastmocks.Manager{} mim := &identitymanagermocks.Manager{} mbi := &blockchainmocks.Plugin{} + mom := &operationmocks.Manager{} mbi.On("GetFFIParamValidator", mock.Anything).Return(ðereum.FFIParamValidator{}, nil) - _, err := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi) + _, err := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi, mom) assert.NoError(t, err) } diff --git a/internal/contracts/operations.go b/internal/contracts/operations.go new file mode 100644 index 0000000000..b14f8c138b --- /dev/null +++ b/internal/contracts/operations.go @@ -0,0 +1,79 @@ +// Copyright © 2022 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 contracts + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +type blockchainInvokeData struct { + Request *fftypes.ContractCallRequest `json:"request"` +} + +func addBlockchainInvokeInputs(op *fftypes.Operation, req *fftypes.ContractCallRequest) (err error) { + var reqJSON []byte + if reqJSON, err = json.Marshal(req); err == nil { + err = json.Unmarshal(reqJSON, &op.Input) + } + return err +} + +func retrieveBlockchainInvokeInputs(ctx context.Context, op *fftypes.Operation) (*fftypes.ContractCallRequest, error) { + var req fftypes.ContractCallRequest + s := op.Input.String() + if err := json.Unmarshal([]byte(s), &req); err != nil { + return nil, i18n.WrapError(ctx, err, i18n.MsgJSONObjectParseFailed, s) + } + return &req, nil +} + +func (cm *contractManager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + switch op.Type { + case fftypes.OpTypeBlockchainInvoke: + req, err := retrieveBlockchainInvokeInputs(ctx, op) + if err != nil { + return nil, err + } + return opBlockchainInvoke(op, req), nil + + default: + return nil, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func (cm *contractManager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) { + switch data := op.Data.(type) { + case blockchainInvokeData: + req := data.Request + return false, cm.blockchain.InvokeContract(ctx, op.ID, req.Key, req.Location, req.Method, req.Input) + + default: + return false, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func opBlockchainInvoke(op *fftypes.Operation, req *fftypes.ContractCallRequest) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: blockchainInvokeData{Request: req}, + } +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 406cc37904..dae342e54c 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -445,23 +445,23 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) { } } - or.syncasync = syncasync.NewSyncAsyncBridge(ctx, or.database, or.data) - or.batchpin = batchpin.NewBatchPinSubmitter(or.database, or.identity, or.blockchain, or.metrics) - if or.operations == nil { if or.operations, err = operations.NewOperationsManager(ctx, or.database, or.tokens); err != nil { return err } } + or.syncasync = syncasync.NewSyncAsyncBridge(ctx, or.database, or.data) + or.batchpin = batchpin.NewBatchPinSubmitter(or.database, or.identity, or.blockchain, or.metrics, or.operations) + if or.messaging == nil { - if or.messaging, err = privatemessaging.NewPrivateMessaging(ctx, or.database, or.identity, or.dataexchange, or.blockchain, or.batch, or.data, or.syncasync, or.batchpin, or.metrics); err != nil { + if or.messaging, err = privatemessaging.NewPrivateMessaging(ctx, or.database, or.identity, or.dataexchange, or.blockchain, or.batch, or.data, or.syncasync, or.batchpin, or.metrics, or.operations); err != nil { return err } } if or.broadcast == nil { - if or.broadcast, err = broadcast.NewBroadcastManager(ctx, or.database, or.identity, or.data, or.blockchain, or.dataexchange, or.publicstorage, or.batch, or.syncasync, or.batchpin, or.metrics); err != nil { + if or.broadcast, err = broadcast.NewBroadcastManager(ctx, or.database, or.identity, or.data, or.blockchain, or.dataexchange, or.publicstorage, or.batch, or.syncasync, or.batchpin, or.metrics, or.operations); err != nil { return err } } @@ -474,7 +474,7 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) { } if or.contracts == nil { - or.contracts, err = contracts.NewContractManager(ctx, or.database, or.publicstorage, or.broadcast, or.identity, or.blockchain) + or.contracts, err = contracts.NewContractManager(ctx, or.database, or.publicstorage, or.broadcast, or.identity, or.blockchain, or.operations) if err != nil { return err } diff --git a/internal/privatemessaging/operations.go b/internal/privatemessaging/operations.go new file mode 100644 index 0000000000..e82e675b73 --- /dev/null +++ b/internal/privatemessaging/operations.go @@ -0,0 +1,152 @@ +// Copyright © 2022 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 privatemessaging + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +type transferBlobData struct { + Node *fftypes.Node `json:"node"` + Blob *fftypes.Blob `json:"blob"` +} + +type batchSendData struct { + Node *fftypes.Node `json:"node"` + Transport *fftypes.TransportWrapper `json:"transport"` +} + +func addTransferBlobInputs(op *fftypes.Operation, nodeID *fftypes.UUID, blobHash *fftypes.Bytes32) { + op.Input = fftypes.JSONObject{ + "node": nodeID.String(), + "blob": blobHash.String(), + } +} + +func retrieveTransferBlobInputs(ctx context.Context, op *fftypes.Operation) (nodeID *fftypes.UUID, blobHash *fftypes.Bytes32, err error) { + nodeID, err = fftypes.ParseUUID(ctx, op.Input.GetString("node")) + if err == nil { + blobHash, err = fftypes.ParseBytes32(ctx, op.Input.GetString("blob")) + } + return nodeID, blobHash, err +} + +func addBatchSendInputs(op *fftypes.Operation, nodeID *fftypes.UUID, groupHash *fftypes.Bytes32, batchID *fftypes.UUID, manifest string) error { + op.Input = fftypes.JSONObject{ + "node": nodeID.String(), + "group": groupHash.String(), + "batch": batchID.String(), + "manifest": manifest, + } + return nil +} + +func retrieveBatchSendInputs(ctx context.Context, op *fftypes.Operation) (nodeID *fftypes.UUID, groupHash *fftypes.Bytes32, batchID *fftypes.UUID, manifest string, err error) { + nodeID, err = fftypes.ParseUUID(ctx, op.Input.GetString("node")) + if err != nil { + return nil, nil, nil, "", err + } + groupHash, err = fftypes.ParseBytes32(ctx, op.Input.GetString("group")) + if err != nil { + return nil, nil, nil, "", err + } + batchID, err = fftypes.ParseUUID(ctx, op.Input.GetString("batch")) + if err != nil { + return nil, nil, nil, "", err + } + manifest = op.Input.GetString("manifest") + return nodeID, groupHash, batchID, manifest, nil +} + +func (pm *privateMessaging) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + switch op.Type { + case fftypes.OpTypePublicStorageBatchBroadcast: + nodeID, blobHash, err := retrieveTransferBlobInputs(ctx, op) + if err != nil { + return nil, err + } + node, err := pm.database.GetNodeByID(ctx, nodeID) + if err != nil { + return nil, err + } + blob, err := pm.database.GetBlobMatchingHash(ctx, blobHash) + if err != nil { + return nil, err + } + return opTransferBlob(op, node, blob), nil + + case fftypes.OpTypeDataExchangeBatchSend: + nodeID, groupHash, batchID, _, err := retrieveBatchSendInputs(ctx, op) + if err != nil { + return nil, err + } + node, err := pm.database.GetNodeByID(ctx, nodeID) + if err != nil { + return nil, err + } + group, err := pm.database.GetGroupByHash(ctx, groupHash) + if err != nil { + return nil, err + } + batch, err := pm.database.GetBatchByID(ctx, batchID) + if err != nil { + return nil, err + } + transport := &fftypes.TransportWrapper{Group: group, Batch: batch} + return opBatchSend(op, node, transport), nil + + default: + return nil, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func (pm *privateMessaging) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) { + switch data := op.Data.(type) { + case transferBlobData: + return false, pm.exchange.TransferBLOB(ctx, op.ID, data.Node.DX.Peer, data.Blob.PayloadRef) + + case batchSendData: + payload, err := json.Marshal(data.Transport) + if err != nil { + return false, i18n.WrapError(ctx, err, i18n.MsgSerializationFailed) + } + return false, pm.exchange.SendMessage(ctx, op.ID, data.Node.DX.Peer, payload) + + default: + return false, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func opTransferBlob(op *fftypes.Operation, node *fftypes.Node, blob *fftypes.Blob) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: transferBlobData{Node: node, Blob: blob}, + } +} + +func opBatchSend(op *fftypes.Operation, node *fftypes.Node, transport *fftypes.TransportWrapper) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: batchSendData{Node: node, Transport: transport}, + } +} diff --git a/internal/privatemessaging/privatemessaging.go b/internal/privatemessaging/privatemessaging.go index ea80bd3c8c..8782616847 100644 --- a/internal/privatemessaging/privatemessaging.go +++ b/internal/privatemessaging/privatemessaging.go @@ -18,7 +18,6 @@ package privatemessaging import ( "context" - "encoding/json" "github.com/hyperledger/firefly/internal/batch" "github.com/hyperledger/firefly/internal/batchpin" @@ -28,6 +27,7 @@ import ( "github.com/hyperledger/firefly/internal/identity" "github.com/hyperledger/firefly/internal/log" "github.com/hyperledger/firefly/internal/metrics" + "github.com/hyperledger/firefly/internal/operations" "github.com/hyperledger/firefly/internal/retry" "github.com/hyperledger/firefly/internal/syncasync" "github.com/hyperledger/firefly/internal/sysmessaging" @@ -48,6 +48,10 @@ type Manager interface { NewMessage(ns string, msg *fftypes.MessageInOut) sysmessaging.MessageSender SendMessage(ctx context.Context, ns string, in *fftypes.MessageInOut, waitConfirm bool) (out *fftypes.Message, err error) RequestReply(ctx context.Context, ns string, request *fftypes.MessageInOut) (reply *fftypes.MessageInOut, err error) + + // From operations.OperationHandler + PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) + RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) } type privateMessaging struct { @@ -68,10 +72,11 @@ type privateMessaging struct { opCorrelationRetries int maxBatchPayloadLength int64 metrics metrics.Manager + operations operations.Manager } -func NewPrivateMessaging(ctx context.Context, di database.Plugin, im identity.Manager, dx dataexchange.Plugin, bi blockchain.Plugin, ba batch.Manager, dm data.Manager, sa syncasync.Bridge, bp batchpin.Submitter, mm metrics.Manager) (Manager, error) { - if di == nil || im == nil || dx == nil || bi == nil || ba == nil || dm == nil { +func NewPrivateMessaging(ctx context.Context, di database.Plugin, im identity.Manager, dx dataexchange.Plugin, bi blockchain.Plugin, ba batch.Manager, dm data.Manager, sa syncasync.Bridge, bp batchpin.Submitter, mm metrics.Manager, om operations.Manager) (Manager, error) { + if di == nil || im == nil || dx == nil || bi == nil || ba == nil || dm == nil || mm == nil || om == nil { return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) } @@ -99,6 +104,7 @@ func NewPrivateMessaging(ctx context.Context, di database.Plugin, im identity.Ma opCorrelationRetries: config.GetInt(config.PrivateMessagingOpCorrelationRetries), maxBatchPayloadLength: config.GetByteSize(config.PrivateMessagingBatchPayloadLimit), metrics: mm, + operations: om, } pm.groupManager.groupCache = ccache.New( // We use a LRU cache with a size-aware max @@ -129,6 +135,11 @@ func NewPrivateMessaging(ctx context.Context, di database.Plugin, im identity.Ma }, pm.dispatchUnpinnedBatch, bo) + om.RegisterHandler(pm, []fftypes.OpType{ + fftypes.OpTypeDataExchangeBlobSend, + fftypes.OpTypeDataExchangeBatchSend, + }) + return pm, nil } @@ -187,13 +198,12 @@ func (pm *privateMessaging) transferBlobs(ctx context.Context, data []*fftypes.D d.Namespace, txid, fftypes.OpTypeDataExchangeBlobSend) + addTransferBlobInputs(op, node.ID, blob.Hash) if err = pm.database.InsertOperation(ctx, op); err != nil { return err } - if err := pm.exchange.TransferBLOB(ctx, op.ID, node.DX.Peer, blob.PayloadRef); err != nil { - return err - } + return pm.operations.RunOperation(ctx, opTransferBlob(op, node, blob)) } } return nil @@ -203,11 +213,6 @@ func (pm *privateMessaging) sendData(ctx context.Context, tw *fftypes.TransportW l := log.L(ctx) batch := tw.Batch - payload, err := json.Marshal(tw) - if err != nil { - return i18n.WrapError(ctx, err, i18n.MsgSerializationFailed) - } - // TODO: move to using DIDs consistently as the way to reference the node/organization (i.e. node.Owner becomes a DID) localOrgSigingKey, err := pm.identity.GetLocalOrgKey(ctx) if err != nil { @@ -234,18 +239,18 @@ func (pm *privateMessaging) sendData(ctx context.Context, tw *fftypes.TransportW batch.Namespace, batch.Payload.TX.ID, fftypes.OpTypeDataExchangeBatchSend) - op.Input = fftypes.JSONObject{ - "manifest": tw.Batch.Manifest().String(), + var groupHash *fftypes.Bytes32 + if tw.Group != nil { + groupHash = tw.Group.Hash } - if err = pm.database.InsertOperation(ctx, op); err != nil { + if err = addBatchSendInputs(op, node.ID, groupHash, batch.ID, tw.Batch.Manifest().String()); err != nil { return err } - - // Send the payload itself - err := pm.exchange.SendMessage(ctx, op.ID, node.DX.Peer, payload) - if err != nil { + if err = pm.database.InsertOperation(ctx, op); err != nil { return err } + + return pm.operations.RunOperation(ctx, opBatchSend(op, node, tw)) } return nil diff --git a/internal/privatemessaging/privatemessaging_test.go b/internal/privatemessaging/privatemessaging_test.go index 093a0333a3..09cb86d9d2 100644 --- a/internal/privatemessaging/privatemessaging_test.go +++ b/internal/privatemessaging/privatemessaging_test.go @@ -30,6 +30,7 @@ import ( "github.com/hyperledger/firefly/mocks/datamocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/mocks/metricsmocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/mocks/syncasyncmocks" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/stretchr/testify/assert" @@ -51,6 +52,7 @@ func newTestPrivateMessagingCommon(t *testing.T, metricsEnabled bool) (*privateM msa := &syncasyncmocks.Bridge{} mbp := &batchpinmocks.Submitter{} mmi := &metricsmocks.Manager{} + mom := &operationmocks.Manager{} mba.On("RegisterDispatcher", pinnedPrivateDispatcherName, @@ -77,7 +79,7 @@ func newTestPrivateMessagingCommon(t *testing.T, metricsEnabled bool) (*privateM } ctx, cancel := context.WithCancel(context.Background()) - pm, err := NewPrivateMessaging(ctx, mdi, mim, mdx, mbi, mba, mdm, msa, mbp, mmi) + pm, err := NewPrivateMessaging(ctx, mdi, mim, mdx, mbi, mba, mdm, msa, mbp, mmi, mom) assert.NoError(t, err) // Default mocks to save boilerplate in the tests @@ -197,7 +199,7 @@ func TestDispatchBatchWithBlobs(t *testing.T) { } func TestNewPrivateMessagingMissingDeps(t *testing.T) { - _, err := NewPrivateMessaging(context.Background(), nil, nil, nil, nil, nil, nil, nil, nil, nil) + _, err := NewPrivateMessaging(context.Background(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) assert.Regexp(t, "FF10128", err) } diff --git a/mocks/batchpinmocks/submitter.go b/mocks/batchpinmocks/submitter.go index 2902e504e2..810d816e85 100644 --- a/mocks/batchpinmocks/submitter.go +++ b/mocks/batchpinmocks/submitter.go @@ -14,6 +14,50 @@ type Submitter struct { mock.Mock } +// PrepareOperation provides a mock function with given fields: ctx, op +func (_m *Submitter) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + ret := _m.Called(ctx, op) + + var r0 *fftypes.PreparedOperation + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) *fftypes.PreparedOperation); ok { + r0 = rf(ctx, op) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.PreparedOperation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.Operation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RunOperation provides a mock function with given fields: ctx, op +func (_m *Submitter) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (bool, error) { + ret := _m.Called(ctx, op) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.PreparedOperation) bool); ok { + r0 = rf(ctx, op) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.PreparedOperation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SubmitPinnedBatch provides a mock function with given fields: ctx, batch, contexts func (_m *Submitter) SubmitPinnedBatch(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error { ret := _m.Called(ctx, batch, contexts) diff --git a/mocks/broadcastmocks/manager.go b/mocks/broadcastmocks/manager.go index f419341a4f..555e3b1aa9 100644 --- a/mocks/broadcastmocks/manager.go +++ b/mocks/broadcastmocks/manager.go @@ -193,6 +193,50 @@ func (_m *Manager) NewBroadcast(ns string, in *fftypes.MessageInOut) sysmessagin return r0 } +// PrepareOperation provides a mock function with given fields: ctx, op +func (_m *Manager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + ret := _m.Called(ctx, op) + + var r0 *fftypes.PreparedOperation + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) *fftypes.PreparedOperation); ok { + r0 = rf(ctx, op) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.PreparedOperation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.Operation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RunOperation provides a mock function with given fields: ctx, op +func (_m *Manager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (bool, error) { + ret := _m.Called(ctx, op) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.PreparedOperation) bool); ok { + r0 = rf(ctx, op) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.PreparedOperation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Start provides a mock function with given fields: func (_m *Manager) Start() error { ret := _m.Called() diff --git a/mocks/contractmocks/manager.go b/mocks/contractmocks/manager.go index af8fca637c..8a2c8ffb63 100644 --- a/mocks/contractmocks/manager.go +++ b/mocks/contractmocks/manager.go @@ -357,6 +357,50 @@ func (_m *Manager) InvokeContractAPI(ctx context.Context, ns string, apiName str return r0, r1 } +// PrepareOperation provides a mock function with given fields: ctx, op +func (_m *Manager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + ret := _m.Called(ctx, op) + + var r0 *fftypes.PreparedOperation + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) *fftypes.PreparedOperation); ok { + r0 = rf(ctx, op) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.PreparedOperation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.Operation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RunOperation provides a mock function with given fields: ctx, op +func (_m *Manager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (bool, error) { + ret := _m.Called(ctx, op) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.PreparedOperation) bool); ok { + r0 = rf(ctx, op) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.PreparedOperation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SubscribeContract provides a mock function with given fields: ctx, ns, eventPath, req func (_m *Manager) SubscribeContract(ctx context.Context, ns string, eventPath string, req *fftypes.ContractSubscribeRequest) (*fftypes.ContractSubscription, error) { ret := _m.Called(ctx, ns, eventPath, req) diff --git a/mocks/privatemessagingmocks/manager.go b/mocks/privatemessagingmocks/manager.go index be51e56a61..afa2c147f8 100644 --- a/mocks/privatemessagingmocks/manager.go +++ b/mocks/privatemessagingmocks/manager.go @@ -110,6 +110,29 @@ func (_m *Manager) NewMessage(ns string, msg *fftypes.MessageInOut) sysmessaging return r0 } +// PrepareOperation provides a mock function with given fields: ctx, op +func (_m *Manager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + ret := _m.Called(ctx, op) + + var r0 *fftypes.PreparedOperation + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) *fftypes.PreparedOperation); ok { + r0 = rf(ctx, op) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.PreparedOperation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.Operation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RequestReply provides a mock function with given fields: ctx, ns, request func (_m *Manager) RequestReply(ctx context.Context, ns string, request *fftypes.MessageInOut) (*fftypes.MessageInOut, error) { ret := _m.Called(ctx, ns, request) @@ -156,6 +179,27 @@ func (_m *Manager) ResolveInitGroup(ctx context.Context, msg *fftypes.Message) ( return r0, r1 } +// RunOperation provides a mock function with given fields: ctx, op +func (_m *Manager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (bool, error) { + ret := _m.Called(ctx, op) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.PreparedOperation) bool); ok { + r0 = rf(ctx, op) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.PreparedOperation) error); ok { + r1 = rf(ctx, op) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SendMessage provides a mock function with given fields: ctx, ns, in, waitConfirm func (_m *Manager) SendMessage(ctx context.Context, ns string, in *fftypes.MessageInOut, waitConfirm bool) (*fftypes.Message, error) { ret := _m.Called(ctx, ns, in, waitConfirm) From 307002fc05b96127e2b487e2082c883e630c06df Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 16 Feb 2022 14:06:43 -0500 Subject: [PATCH 04/10] Move some operation helpers from txcommon to operations Signed-off-by: Andrew Richardson --- internal/operations/manager.go | 20 ++++++++++++----- internal/operations/manager_test.go | 35 +++++++++++++++++++++++++++++ internal/txcommon/txcommon.go | 14 ------------ internal/txcommon/txcommon_test.go | 31 ------------------------- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/internal/operations/manager.go b/internal/operations/manager.go index f0522d061e..ea6d5a4007 100644 --- a/internal/operations/manager.go +++ b/internal/operations/manager.go @@ -20,7 +20,7 @@ import ( "context" "github.com/hyperledger/firefly/internal/i18n" - "github.com/hyperledger/firefly/internal/txcommon" + "github.com/hyperledger/firefly/internal/log" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/hyperledger/firefly/pkg/tokens" @@ -40,7 +40,6 @@ type Manager interface { type operationsManager struct { ctx context.Context database database.Plugin - txHelper txcommon.Helper tokens map[string]tokens.Plugin handlers map[fftypes.OpType]OperationHandler } @@ -52,7 +51,6 @@ func NewOperationsManager(ctx context.Context, di database.Plugin, ti map[string om := &operationsManager{ ctx: ctx, database: di, - txHelper: txcommon.NewTransactionHelper(di), tokens: ti, handlers: make(map[fftypes.OpType]OperationHandler), } @@ -79,10 +77,22 @@ func (om *operationsManager) RunOperation(ctx context.Context, op *fftypes.Prepa return i18n.NewError(ctx, i18n.MsgOperationNotSupported) } if complete, err := handler.RunOperation(ctx, op); err != nil { - om.txHelper.WriteOperationFailure(ctx, op.ID, err) + om.writeOperationFailure(ctx, op.ID, err) return err } else if complete { - om.txHelper.WriteOperationSuccess(ctx, op.ID, nil) + om.writeOperationSuccess(ctx, op.ID, nil) } return nil } + +func (om *operationsManager) writeOperationSuccess(ctx context.Context, opID *fftypes.UUID, output fftypes.JSONObject) { + if err := om.database.ResolveOperation(ctx, opID, fftypes.OpStatusSucceeded, "", output); err != nil { + log.L(ctx).Errorf("Failed to update operation %s: %s", opID, err) + } +} + +func (om *operationsManager) writeOperationFailure(ctx context.Context, opID *fftypes.UUID, err error) { + if err := om.database.ResolveOperation(ctx, opID, fftypes.OpStatusFailed, err.Error(), nil); err != nil { + log.L(ctx).Errorf("Failed to update operation %s: %s", opID, err) + } +} diff --git a/internal/operations/manager_test.go b/internal/operations/manager_test.go index d5580344b1..19dfc52037 100644 --- a/internal/operations/manager_test.go +++ b/internal/operations/manager_test.go @@ -16,6 +16,7 @@ package operations import ( "context" + "fmt" "testing" "github.com/hyperledger/firefly/internal/config" @@ -24,6 +25,7 @@ import ( "github.com/hyperledger/firefly/pkg/fftypes" "github.com/hyperledger/firefly/pkg/tokens" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func newTestOperations(t *testing.T) (*operationsManager, func()) { @@ -46,3 +48,36 @@ func TestPrepareOperationNotSupported(t *testing.T) { _, err := om.PrepareOperation(context.Background(), op) assert.Regexp(t, "FF10346", err) } + +func TestWriteOperationSuccess(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + opID := fftypes.NewUUID() + output := fftypes.JSONObject{"some": "info"} + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("ResolveOperation", ctx, opID, fftypes.OpStatusSucceeded, "", output).Return(fmt.Errorf("pop")) + + om.writeOperationSuccess(ctx, opID, output) + + mdi.AssertExpectations(t) + +} + +func TestWriteOperationFailure(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + opID := fftypes.NewUUID() + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("ResolveOperation", ctx, opID, fftypes.OpStatusFailed, "pop", mock.Anything).Return(fmt.Errorf("pop")) + + om.writeOperationFailure(ctx, opID, fmt.Errorf("pop")) + + mdi.AssertExpectations(t) + +} diff --git a/internal/txcommon/txcommon.go b/internal/txcommon/txcommon.go index b2ad03856d..28a10805a6 100644 --- a/internal/txcommon/txcommon.go +++ b/internal/txcommon/txcommon.go @@ -29,8 +29,6 @@ type Helper interface { SubmitNewTransaction(ctx context.Context, ns string, txType fftypes.TransactionType) (*fftypes.UUID, error) PersistTransaction(ctx context.Context, ns string, id *fftypes.UUID, txType fftypes.TransactionType, blockchainTXID string) (valid bool, err error) AddBlockchainTX(ctx context.Context, id *fftypes.UUID, blockchainTXID string) error - WriteOperationSuccess(ctx context.Context, opID *fftypes.UUID, output fftypes.JSONObject) - WriteOperationFailure(ctx context.Context, opID *fftypes.UUID, err error) } type transactionHelper struct { @@ -128,15 +126,3 @@ func (t *transactionHelper) AddBlockchainTX(ctx context.Context, id *fftypes.UUI return nil } - -func (t *transactionHelper) WriteOperationSuccess(ctx context.Context, opID *fftypes.UUID, output fftypes.JSONObject) { - if err2 := t.database.ResolveOperation(ctx, opID, fftypes.OpStatusSucceeded, "", output); err2 != nil { - log.L(ctx).Errorf("Failed to update operation %s: %s", opID, err2) - } -} - -func (t *transactionHelper) WriteOperationFailure(ctx context.Context, opID *fftypes.UUID, err error) { - if err2 := t.database.ResolveOperation(ctx, opID, fftypes.OpStatusFailed, err.Error(), nil); err2 != nil { - log.L(ctx).Errorf("Failed to update operation %s: %s", opID, err2) - } -} diff --git a/internal/txcommon/txcommon_test.go b/internal/txcommon/txcommon_test.go index e9982eb204..8576358ebd 100644 --- a/internal/txcommon/txcommon_test.go +++ b/internal/txcommon/txcommon_test.go @@ -375,34 +375,3 @@ func TestAddBlockchainTXUnchanged(t *testing.T) { mdi.AssertExpectations(t) } - -func TestWriteOperationSuccess(t *testing.T) { - - mdi := &databasemocks.Plugin{} - txHelper := NewTransactionHelper(mdi) - ctx := context.Background() - - opID := fftypes.NewUUID() - output := fftypes.JSONObject{"some": "info"} - mdi.On("ResolveOperation", ctx, opID, fftypes.OpStatusSucceeded, "", output).Return(fmt.Errorf("pop")) - - txHelper.WriteOperationSuccess(ctx, opID, output) - - mdi.AssertExpectations(t) - -} - -func TestWriteOperationFailure(t *testing.T) { - - mdi := &databasemocks.Plugin{} - txHelper := NewTransactionHelper(mdi) - ctx := context.Background() - - opID := fftypes.NewUUID() - mdi.On("ResolveOperation", ctx, opID, fftypes.OpStatusFailed, "pop", mock.Anything).Return(fmt.Errorf("pop")) - - txHelper.WriteOperationFailure(ctx, opID, fmt.Errorf("pop")) - - mdi.AssertExpectations(t) - -} From 40ebfff6469cff22b1c5289b9c312530632ddea7 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 16 Feb 2022 15:35:54 -0500 Subject: [PATCH 05/10] Fix broken unit tests Signed-off-by: Andrew Richardson --- internal/batchpin/batchpin.go | 8 +- internal/batchpin/batchpin_test.go | 33 ++++++-- internal/broadcast/manager_test.go | 52 +++++------- internal/contracts/manager_test.go | 40 +++++++--- internal/orchestrator/orchestrator.go | 7 +- internal/orchestrator/orchestrator_test.go | 12 +++ internal/privatemessaging/message_test.go | 75 +++++------------ internal/privatemessaging/operations.go | 3 +- internal/privatemessaging/privatemessaging.go | 8 +- .../privatemessaging/privatemessaging_test.go | 80 +++++++++++-------- 10 files changed, 172 insertions(+), 146 deletions(-) diff --git a/internal/batchpin/batchpin.go b/internal/batchpin/batchpin.go index e14142892a..9fb2b031a4 100644 --- a/internal/batchpin/batchpin.go +++ b/internal/batchpin/batchpin.go @@ -19,6 +19,7 @@ package batchpin import ( "context" + "github.com/hyperledger/firefly/internal/i18n" "github.com/hyperledger/firefly/internal/identity" "github.com/hyperledger/firefly/internal/metrics" "github.com/hyperledger/firefly/internal/operations" @@ -43,7 +44,10 @@ type batchPinSubmitter struct { operations operations.Manager } -func NewBatchPinSubmitter(di database.Plugin, im identity.Manager, bi blockchain.Plugin, mm metrics.Manager, om operations.Manager) Submitter { +func NewBatchPinSubmitter(ctx context.Context, di database.Plugin, im identity.Manager, bi blockchain.Plugin, mm metrics.Manager, om operations.Manager) (Submitter, error) { + if di == nil || im == nil || bi == nil || mm == nil || om == nil { + return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) + } bp := &batchPinSubmitter{ database: di, identity: im, @@ -54,7 +58,7 @@ func NewBatchPinSubmitter(di database.Plugin, im identity.Manager, bi blockchain om.RegisterHandler(bp, []fftypes.OpType{ fftypes.OpTypeBlockchainBatchPin, }) - return bp + return bp, nil } func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error { diff --git a/internal/batchpin/batchpin_test.go b/internal/batchpin/batchpin_test.go index 2b0ff17161..51d778fbd8 100644 --- a/internal/batchpin/batchpin_test.go +++ b/internal/batchpin/batchpin_test.go @@ -41,20 +41,23 @@ func newTestBatchPinSubmitter(t *testing.T, enableMetrics bool) *batchPinSubmitt mmi := &metricsmocks.Manager{} mom := &operationmocks.Manager{} mmi.On("IsMetricsEnabled").Return(enableMetrics) + mom.On("RegisterHandler", mock.Anything, mock.Anything) if enableMetrics { mmi.On("CountBatchPin").Return() } mbi.On("Name").Return("ut").Maybe() - bps := NewBatchPinSubmitter(mdi, mim, mbi, mmi, mom).(*batchPinSubmitter) - return bps + bps, err := NewBatchPinSubmitter(context.Background(), mdi, mim, mbi, mmi, mom) + assert.NoError(t, err) + return bps.(*batchPinSubmitter) } func TestSubmitPinnedBatchOk(t *testing.T) { bp := newTestBatchPinSubmitter(t, false) ctx := context.Background() - mbi := bp.blockchain.(*blockchainmocks.Plugin) mdi := bp.database.(*databasemocks.Plugin) + mmi := bp.metrics.(*metricsmocks.Manager) + mom := bp.operations.(*operationmocks.Manager) batch := &fftypes.Batch{ ID: fftypes.NewUUID(), @@ -76,20 +79,28 @@ func TestSubmitPinnedBatchOk(t *testing.T) { assert.Equal(t, *batch.Payload.TX.ID, *op.Transaction) return true })).Return(nil) - mbi.On("SubmitBatchPin", ctx, mock.Anything, (*fftypes.UUID)(nil), "0x12345", mock.Anything).Return(nil) - mmi := bp.metrics.(*metricsmocks.Manager) mmi.On("IsMetricsEnabled").Return(false) + mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(batchPinData) + return op.Type == fftypes.OpTypeBlockchainBatchPin && data.Batch == batch + })).Return(nil) + err := bp.SubmitPinnedBatch(ctx, batch, contexts) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mmi.AssertExpectations(t) + mom.AssertExpectations(t) } func TestSubmitPinnedBatchWithMetricsOk(t *testing.T) { bp := newTestBatchPinSubmitter(t, true) ctx := context.Background() - mbi := bp.blockchain.(*blockchainmocks.Plugin) mdi := bp.database.(*databasemocks.Plugin) mmi := bp.metrics.(*metricsmocks.Manager) + mom := bp.operations.(*operationmocks.Manager) + batch := &fftypes.Batch{ ID: fftypes.NewUUID(), Identity: fftypes.Identity{ @@ -110,12 +121,18 @@ func TestSubmitPinnedBatchWithMetricsOk(t *testing.T) { assert.Equal(t, *batch.Payload.TX.ID, *op.Transaction) return true })).Return(nil) - mbi.On("SubmitBatchPin", ctx, mock.Anything, (*fftypes.UUID)(nil), "0x12345", mock.Anything).Return(nil) mmi.On("IsMetricsEnabled").Return(true) - mmi.On("BatchPinCounter").Return() + mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(batchPinData) + return op.Type == fftypes.OpTypeBlockchainBatchPin && data.Batch == batch + })).Return(nil) err := bp.SubmitPinnedBatch(ctx, batch, contexts) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mmi.AssertExpectations(t) + mom.AssertExpectations(t) } func TestSubmitPinnedBatchOpFail(t *testing.T) { diff --git a/internal/broadcast/manager_test.go b/internal/broadcast/manager_test.go index a1444d4c37..bbf1c9373d 100644 --- a/internal/broadcast/manager_test.go +++ b/internal/broadcast/manager_test.go @@ -66,6 +66,7 @@ func newTestBroadcastCommon(t *testing.T, metricsEnabled bool) (*broadcastManage fftypes.MessageTypeDefinition, fftypes.MessageTypeTransferBroadcast, }, mock.Anything, mock.Anything).Return() + mom.On("RegisterHandler", mock.Anything, mock.Anything) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { @@ -137,29 +138,6 @@ func TestBroadcastMessageBad(t *testing.T) { } -func TestDispatchBatchInvalidData(t *testing.T) { - bm, cancel := newTestBroadcast(t) - defer cancel() - - err := bm.dispatchBatch(context.Background(), &fftypes.Batch{ - Payload: fftypes.BatchPayload{ - Data: []*fftypes.Data{ - {Value: fftypes.JSONAnyPtr(`!json`)}, - }, - }, - }, []*fftypes.Bytes32{fftypes.NewRandB32()}) - assert.Regexp(t, "FF10137", err) -} - -func TestDispatchBatchUploadFail(t *testing.T) { - bm, cancel := newTestBroadcast(t) - defer cancel() - bm.publicstorage.(*publicstoragemocks.Plugin).On("PublishData", mock.Anything, mock.Anything).Return("", fmt.Errorf("pop")) - - err := bm.dispatchBatch(context.Background(), &fftypes.Batch{}, []*fftypes.Bytes32{fftypes.NewRandB32()}) - assert.EqualError(t, err, "pop") -} - func TestDispatchBatchSubmitBatchPinSucceed(t *testing.T) { bm, cancel := newTestBroadcast(t) defer cancel() @@ -169,31 +147,45 @@ func TestDispatchBatchSubmitBatchPinSucceed(t *testing.T) { } mdi := bm.database.(*databasemocks.Plugin) - mps := bm.publicstorage.(*publicstoragemocks.Plugin) mbp := bm.batchpin.(*batchpinmocks.Submitter) - mps.On("PublishData", mock.Anything, mock.Anything).Return("id1", nil) - mdi.On("UpdateBatch", mock.Anything, batch.ID, mock.Anything).Return(nil) + mom := bm.operations.(*operationmocks.Manager) mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) mbp.On("SubmitPinnedBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(batchBroadcastData) + return op.Type == fftypes.OpTypePublicStorageBatchBroadcast && data.Batch == batch + })).Return(nil) err := bm.dispatchBatch(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mbp.AssertExpectations(t) + mom.AssertExpectations(t) } func TestDispatchBatchSubmitBroadcastFail(t *testing.T) { bm, cancel := newTestBroadcast(t) defer cancel() + batch := &fftypes.Batch{Identity: fftypes.Identity{Author: "wrong", Key: "wrong"}} + mdi := bm.database.(*databasemocks.Plugin) - mps := bm.publicstorage.(*publicstoragemocks.Plugin) mbp := bm.batchpin.(*batchpinmocks.Submitter) - mps.On("PublishData", mock.Anything, mock.Anything).Return("id1", nil) - mdi.On("UpdateBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mom := bm.operations.(*operationmocks.Manager) mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) mbp.On("SubmitPinnedBatch", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(batchBroadcastData) + return op.Type == fftypes.OpTypePublicStorageBatchBroadcast && data.Batch == batch + })).Return(nil) - err := bm.dispatchBatch(context.Background(), &fftypes.Batch{Identity: fftypes.Identity{Author: "wrong", Key: "wrong"}}, []*fftypes.Bytes32{fftypes.NewRandB32()}) + err := bm.dispatchBatch(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) + mbp.AssertExpectations(t) + mom.AssertExpectations(t) } func TestPublishBlobsUpdateDataFail(t *testing.T) { diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index 308d09f5e3..9aea7a84ff 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -44,6 +44,7 @@ func newTestContractManager() *contractManager { mbi := &blockchainmocks.Plugin{} mom := &operationmocks.Manager{} mbi.On("GetFFIParamValidator", mock.Anything).Return(nil, nil) + mom.On("RegisterHandler", mock.Anything, mock.Anything) mbi.On("Name").Return("mockblockchain").Maybe() @@ -83,6 +84,7 @@ func TestNewContractManagerFFISchemaLoader(t *testing.T) { mbi := &blockchainmocks.Plugin{} mom := &operationmocks.Manager{} mbi.On("GetFFIParamValidator", mock.Anything).Return(ðereum.FFIParamValidator{}, nil) + mom.On("RegisterHandler", mock.Anything, mock.Anything) _, err := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi, mom) assert.NoError(t, err) } @@ -976,10 +978,10 @@ func TestGetFFIs(t *testing.T) { func TestInvokeContract(t *testing.T) { cm := newTestContractManager() - mbi := cm.blockchain.(*blockchainmocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) mdi := cm.database.(*databasemocks.Plugin) mth := cm.txHelper.(*txcommonmocks.Helper) + mom := cm.operations.(*operationmocks.Manager) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeInvoke, @@ -995,25 +997,31 @@ func TestInvokeContract(t *testing.T) { } mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(fftypes.NewUUID(), nil) - mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Namespace == "ns1" && op.Type == fftypes.OpTypeBlockchainInvoke && op.Plugin == "mockblockchain" })).Return(nil) - mbi.On("InvokeContract", mock.Anything, mock.AnythingOfType("*fftypes.UUID"), "key-resolved", req.Location, req.Method, req.Input).Return(nil) + mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(blockchainInvokeData) + return op.Type == fftypes.OpTypeBlockchainInvoke && data.Request == req + })).Return(nil) _, err := cm.InvokeContract(context.Background(), "ns1", req) assert.NoError(t, err) + mth.AssertExpectations(t) + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mom.AssertExpectations(t) } func TestInvokeContractFail(t *testing.T) { cm := newTestContractManager() - mbi := cm.blockchain.(*blockchainmocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) mdi := cm.database.(*databasemocks.Plugin) mth := cm.txHelper.(*txcommonmocks.Helper) + mom := cm.operations.(*operationmocks.Manager) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeInvoke, @@ -1029,18 +1037,23 @@ func TestInvokeContractFail(t *testing.T) { } mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(fftypes.NewUUID(), nil) - mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Namespace == "ns1" && op.Type == fftypes.OpTypeBlockchainInvoke && op.Plugin == "mockblockchain" })).Return(nil) - mbi.On("InvokeContract", mock.Anything, mock.AnythingOfType("*fftypes.UUID"), "key-resolved", req.Location, req.Method, req.Input).Return(fmt.Errorf("pop")) - mth.On("WriteOperationFailure", mock.Anything, mock.Anything, fmt.Errorf("pop")) + mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(blockchainInvokeData) + return op.Type == fftypes.OpTypeBlockchainInvoke && data.Request == req + })).Return(fmt.Errorf("pop")) _, err := cm.InvokeContract(context.Background(), "ns1", req) assert.EqualError(t, err, "pop") + + mim.AssertExpectations(t) + mdi.AssertExpectations(t) mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestInvokeContractFailResolveSigningKey(t *testing.T) { @@ -1365,9 +1378,9 @@ func TestInvokeContractAPI(t *testing.T) { cm := newTestContractManager() mdb := cm.database.(*databasemocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) - mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) mth := cm.txHelper.(*txcommonmocks.Helper) + mom := cm.operations.(*operationmocks.Manager) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeInvoke, @@ -1393,11 +1406,20 @@ func TestInvokeContractAPI(t *testing.T) { mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Namespace == "ns1" && op.Type == fftypes.OpTypeBlockchainInvoke && op.Plugin == "mockblockchain" })).Return(nil) - mbi.On("InvokeContract", mock.Anything, mock.AnythingOfType("*fftypes.UUID"), "key-resolved", req.Location, mock.AnythingOfType("*fftypes.FFIMethod"), req.Input).Return(nil) + mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(blockchainInvokeData) + return op.Type == fftypes.OpTypeBlockchainInvoke && data.Request == req + })).Return(nil) _, err := cm.InvokeContractAPI(context.Background(), "ns1", "banana", "peel", req) assert.NoError(t, err) + + mdb.AssertExpectations(t) + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestInvokeContractAPIFailContractLookup(t *testing.T) { diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index dae342e54c..2fe102ab5a 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -452,7 +452,12 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) { } or.syncasync = syncasync.NewSyncAsyncBridge(ctx, or.database, or.data) - or.batchpin = batchpin.NewBatchPinSubmitter(or.database, or.identity, or.blockchain, or.metrics, or.operations) + + if or.batchpin == nil { + if or.batchpin, err = batchpin.NewBatchPinSubmitter(ctx, or.database, or.identity, or.blockchain, or.metrics, or.operations); err != nil { + return err + } + } if or.messaging == nil { if or.messaging, err = privatemessaging.NewPrivateMessaging(ctx, or.database, or.identity, or.dataexchange, or.blockchain, or.batch, or.data, or.syncasync, or.batchpin, or.metrics, or.operations); err != nil { diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index c6e8888738..2c0dc6e612 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly/internal/tokens/tifactory" "github.com/hyperledger/firefly/mocks/assetmocks" "github.com/hyperledger/firefly/mocks/batchmocks" + "github.com/hyperledger/firefly/mocks/batchpinmocks" "github.com/hyperledger/firefly/mocks/blockchainmocks" "github.com/hyperledger/firefly/mocks/broadcastmocks" "github.com/hyperledger/firefly/mocks/contractmocks" @@ -70,6 +71,7 @@ type testOrchestrator struct { mcm *contractmocks.Manager mmi *metricsmocks.Manager mom *operationmocks.Manager + mbp *batchpinmocks.Submitter } func newTestOrchestrator() *testOrchestrator { @@ -97,6 +99,7 @@ func newTestOrchestrator() *testOrchestrator { mcm: &contractmocks.Manager{}, mmi: &metricsmocks.Manager{}, mom: &operationmocks.Manager{}, + mbp: &batchpinmocks.Submitter{}, } tor.orchestrator.database = tor.mdi tor.orchestrator.data = tor.mdm @@ -115,6 +118,7 @@ func newTestOrchestrator() *testOrchestrator { tor.orchestrator.tokens = map[string]tokens.Plugin{"token": tor.mti} tor.orchestrator.metrics = tor.mmi tor.orchestrator.operations = tor.mom + tor.orchestrator.batchpin = tor.mbp tor.mdi.On("Name").Return("mock-di").Maybe() tor.mem.On("Name").Return("mock-ei").Maybe() tor.mps.On("Name").Return("mock-ps").Maybe() @@ -515,6 +519,14 @@ func TestInitContractsComponentFail(t *testing.T) { assert.Regexp(t, "FF10128", err) } +func TestInitBatchPinComponentFail(t *testing.T) { + or := newTestOrchestrator() + or.database = nil + or.batchpin = nil + err := or.initComponents(context.Background()) + assert.Regexp(t, "FF10128", err) +} + func TestInitOperationsComponentFail(t *testing.T) { or := newTestOrchestrator() or.database = nil diff --git a/internal/privatemessaging/message_test.go b/internal/privatemessaging/message_test.go index ddc0396636..6abf2b33bf 100644 --- a/internal/privatemessaging/message_test.go +++ b/internal/privatemessaging/message_test.go @@ -25,6 +25,7 @@ import ( "github.com/hyperledger/firefly/mocks/dataexchangemocks" "github.com/hyperledger/firefly/mocks/datamocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" + "github.com/hyperledger/firefly/mocks/operationmocks" "github.com/hyperledger/firefly/mocks/syncasyncmocks" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" @@ -660,53 +661,6 @@ func TestRequestReplySuccess(t *testing.T) { assert.NoError(t, err) } -func TestDispatchedUnpinnedMessageMarshalFail(t *testing.T) { - - pm, cancel := newTestPrivateMessaging(t) - defer cancel() - - mim := pm.identity.(*identitymanagermocks.Manager) - mim.On("ResolveInputIdentity", pm.ctx, mock.MatchedBy(func(identity *fftypes.Identity) bool { - assert.Equal(t, "localorg", identity.Author) - return true - })).Return(nil) - - groupID := fftypes.NewRandB32() - nodeID1 := fftypes.NewUUID() - nodeID2 := fftypes.NewUUID() - - mdi := pm.database.(*databasemocks.Plugin) - mdi.On("GetGroupByHash", pm.ctx, groupID).Return(&fftypes.Group{ - Hash: groupID, - GroupIdentity: fftypes.GroupIdentity{ - Members: fftypes.Members{ - {Node: nodeID1, Identity: "localorg"}, - {Node: nodeID2, Identity: "remoteorg"}, - }, - }, - }, nil).Once() - mdi.On("GetNodeByID", pm.ctx, nodeID1).Return(&fftypes.Node{ - ID: nodeID1, Name: "node1", Owner: "localorg", DX: fftypes.DXInfo{Peer: "peer1-local"}, - }, nil).Once() - mdi.On("GetNodeByID", pm.ctx, nodeID2).Return(&fftypes.Node{ - ID: nodeID2, Name: "node2", Owner: "org1", DX: fftypes.DXInfo{Peer: "peer2-remote"}, - }, nil).Once() - - err := pm.dispatchUnpinnedBatch(pm.ctx, &fftypes.Batch{ - ID: fftypes.NewUUID(), - Group: groupID, - Payload: fftypes.BatchPayload{ - Data: []*fftypes.Data{ - {Value: fftypes.JSONAnyPtr("!Bad JSON")}, - }, - }, - }, []*fftypes.Bytes32{}) - assert.Regexp(t, "FF10137", err) - - mdi.AssertExpectations(t) - -} - func TestDispatchedUnpinnedMessageOK(t *testing.T) { pm, cancel := newTestPrivateMessaging(t) @@ -727,6 +681,7 @@ func TestDispatchedUnpinnedMessageOK(t *testing.T) { nodeID2 := fftypes.NewUUID() mdi := pm.database.(*databasemocks.Plugin) + mom := pm.operations.(*operationmocks.Manager) mdi.On("GetGroupByHash", pm.ctx, groupID).Return(&fftypes.Group{ Hash: groupID, GroupIdentity: fftypes.GroupIdentity{ @@ -743,6 +698,10 @@ func TestDispatchedUnpinnedMessageOK(t *testing.T) { ID: nodeID2, Name: "node2", Owner: "org1", DX: fftypes.DXInfo{Peer: "peer2-remote"}, }, nil).Once() mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) + mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(batchSendData) + return op.Type == fftypes.OpTypeDataExchangeBatchSend && *data.Node.ID == *nodeID2 + })).Return(nil) err := pm.dispatchUnpinnedBatch(pm.ctx, &fftypes.Batch{ ID: fftypes.NewUUID(), @@ -768,6 +727,7 @@ func TestDispatchedUnpinnedMessageOK(t *testing.T) { assert.NoError(t, err) mdi.AssertExpectations(t) + mom.AssertExpectations(t) } @@ -828,21 +788,20 @@ func TestSendDataTransferFail(t *testing.T) { pm, cancel := newTestPrivateMessaging(t) defer cancel() + groupID := fftypes.NewRandB32() + nodeID2 := fftypes.NewUUID() + mim := pm.identity.(*identitymanagermocks.Manager) - mim.On("ResolveInputIdentity", pm.ctx, mock.MatchedBy(func(identity *fftypes.Identity) bool { - assert.Equal(t, "localorg", identity.Author) - return true - })).Return(nil) mim.On("GetLocalOrgKey", pm.ctx).Return("localorg", nil) mdi := pm.database.(*databasemocks.Plugin) mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) - mdx := pm.exchange.(*dataexchangemocks.Plugin) - mdx.On("SendMessage", pm.ctx, mock.Anything, "peer2-remote", mock.Anything).Return(fmt.Errorf("pop")) - - groupID := fftypes.NewRandB32() - nodeID2 := fftypes.NewUUID() + mom := pm.operations.(*operationmocks.Manager) + mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(batchSendData) + return op.Type == fftypes.OpTypeDataExchangeBatchSend && *data.Node.ID == *nodeID2 + })).Return(fmt.Errorf("pop")) nodes := []*fftypes.Node{{ ID: nodeID2, Name: "node2", Owner: "org1", DX: fftypes.DXInfo{Peer: "peer2-remote"}, @@ -869,7 +828,9 @@ func TestSendDataTransferFail(t *testing.T) { }, nodes) assert.Regexp(t, "pop", err) - mdx.AssertExpectations(t) + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mom.AssertExpectations(t) } diff --git a/internal/privatemessaging/operations.go b/internal/privatemessaging/operations.go index e82e675b73..1f0789292b 100644 --- a/internal/privatemessaging/operations.go +++ b/internal/privatemessaging/operations.go @@ -49,14 +49,13 @@ func retrieveTransferBlobInputs(ctx context.Context, op *fftypes.Operation) (nod return nodeID, blobHash, err } -func addBatchSendInputs(op *fftypes.Operation, nodeID *fftypes.UUID, groupHash *fftypes.Bytes32, batchID *fftypes.UUID, manifest string) error { +func addBatchSendInputs(op *fftypes.Operation, nodeID *fftypes.UUID, groupHash *fftypes.Bytes32, batchID *fftypes.UUID, manifest string) { op.Input = fftypes.JSONObject{ "node": nodeID.String(), "group": groupHash.String(), "batch": batchID.String(), "manifest": manifest, } - return nil } func retrieveBatchSendInputs(ctx context.Context, op *fftypes.Operation) (nodeID *fftypes.UUID, groupHash *fftypes.Bytes32, batchID *fftypes.UUID, manifest string, err error) { diff --git a/internal/privatemessaging/privatemessaging.go b/internal/privatemessaging/privatemessaging.go index 8782616847..904f39507d 100644 --- a/internal/privatemessaging/privatemessaging.go +++ b/internal/privatemessaging/privatemessaging.go @@ -152,7 +152,6 @@ func (pm *privateMessaging) dispatchPinnedBatch(ctx context.Context, batch *ffty if err != nil { return err } - return pm.batchpin.SubmitPinnedBatch(ctx, batch, contexts) } @@ -243,14 +242,13 @@ func (pm *privateMessaging) sendData(ctx context.Context, tw *fftypes.TransportW if tw.Group != nil { groupHash = tw.Group.Hash } - if err = addBatchSendInputs(op, node.ID, groupHash, batch.ID, tw.Batch.Manifest().String()); err != nil { + addBatchSendInputs(op, node.ID, groupHash, batch.ID, tw.Batch.Manifest().String()) + if err = pm.database.InsertOperation(ctx, op); err != nil { return err } - if err = pm.database.InsertOperation(ctx, op); err != nil { + if err = pm.operations.RunOperation(ctx, opBatchSend(op, node, tw)); err != nil { return err } - - return pm.operations.RunOperation(ctx, opBatchSend(op, node, tw)) } return nil diff --git a/internal/privatemessaging/privatemessaging_test.go b/internal/privatemessaging/privatemessaging_test.go index 09cb86d9d2..e107f6be23 100644 --- a/internal/privatemessaging/privatemessaging_test.go +++ b/internal/privatemessaging/privatemessaging_test.go @@ -70,6 +70,7 @@ func newTestPrivateMessagingCommon(t *testing.T, metricsEnabled bool) (*privateM fftypes.MessageTypePrivate, }, mock.Anything, mock.Anything).Return() mmi.On("IsMetricsEnabled").Return(metricsEnabled) + mom.On("RegisterHandler", mock.Anything, mock.Anything) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { @@ -120,12 +121,8 @@ func TestDispatchBatchWithBlobs(t *testing.T) { mbp := pm.batchpin.(*batchpinmocks.Submitter) mdx := pm.exchange.(*dataexchangemocks.Plugin) mim := pm.identity.(*identitymanagermocks.Manager) + mom := pm.operations.(*operationmocks.Manager) - mim.On("ResolveInputIdentity", pm.ctx, mock.Anything).Run(func(args mock.Arguments) { - identity := args[1].(*fftypes.Identity) - assert.Equal(t, "org1", identity.Author) - identity.Key = "0x12345" - }).Return(nil) mim.On("GetLocalOrgKey", pm.ctx).Return("localorg", nil) mdi.On("GetGroupByHash", pm.ctx, groupID).Return(&fftypes.Group{ Hash: fftypes.NewRandB32(), @@ -155,23 +152,46 @@ func TestDispatchBatchWithBlobs(t *testing.T) { Hash: blob1, PayloadRef: "/blob/1", }, nil) - mdx.On("TransferBLOB", pm.ctx, mock.Anything, "node1", "/blob/1").Return(nil).Once() mdi.On("InsertOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBlobSend })).Return(nil, nil) - mdx.On("TransferBLOB", pm.ctx, mock.Anything, "node2", "/blob/1").Return(nil).Once() mdi.On("InsertOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBlobSend })).Return(nil, nil) - - mdx.On("SendMessage", pm.ctx, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() mdi.On("InsertOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBatchSend })).Return(nil, nil) - mdx.On("SendMessage", pm.ctx, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() mdi.On("InsertOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBatchSend })).Return(nil, nil) + mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + if op.Type != fftypes.OpTypeDataExchangeBlobSend { + return false + } + data := op.Data.(transferBlobData) + return *data.Node.ID == *node1 + })).Return(nil) + mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + if op.Type != fftypes.OpTypeDataExchangeBlobSend { + return false + } + data := op.Data.(transferBlobData) + return *data.Node.ID == *node2 + })).Return(nil) + mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + if op.Type != fftypes.OpTypeDataExchangeBatchSend { + return false + } + data := op.Data.(batchSendData) + return *data.Node.ID == *node1 + })).Return(nil) + mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + if op.Type != fftypes.OpTypeDataExchangeBatchSend { + return false + } + data := op.Data.(batchSendData) + return *data.Node.ID == *node2 + })).Return(nil) mbp.On("SubmitPinnedBatch", pm.ctx, mock.Anything, mock.Anything).Return(nil) @@ -195,7 +215,10 @@ func TestDispatchBatchWithBlobs(t *testing.T) { assert.NoError(t, err) mdi.AssertExpectations(t) + mbp.AssertExpectations(t) mdx.AssertExpectations(t) + mim.AssertExpectations(t) + mom.AssertExpectations(t) } func TestNewPrivateMessagingMissingDeps(t *testing.T) { @@ -203,25 +226,6 @@ func TestNewPrivateMessagingMissingDeps(t *testing.T) { assert.Regexp(t, "FF10128", err) } -func TestDispatchBatchBadData(t *testing.T) { - pm, cancel := newTestPrivateMessaging(t) - defer cancel() - - groupID := fftypes.NewRandB32() - mdi := pm.database.(*databasemocks.Plugin) - mdi.On("GetGroupByHash", pm.ctx, groupID).Return(&fftypes.Group{}, nil) - - err := pm.dispatchPinnedBatch(pm.ctx, &fftypes.Batch{ - Group: groupID, - Payload: fftypes.BatchPayload{ - Data: []*fftypes.Data{ - {Value: fftypes.JSONAnyPtr(`{!json}`)}, - }, - }, - }, []*fftypes.Bytes32{}) - assert.Regexp(t, "FF10137", err) -} - func TestDispatchErrorFindingGroup(t *testing.T) { pm, cancel := newTestPrivateMessaging(t) defer cancel() @@ -385,15 +389,27 @@ func TestTransferBlobsFail(t *testing.T) { defer cancel() mdi := pm.database.(*databasemocks.Plugin) + mom := pm.operations.(*operationmocks.Manager) + + node := &fftypes.Node{ID: fftypes.NewUUID(), DX: fftypes.DXInfo{Peer: "peer1"}} + mdi.On("GetBlobMatchingHash", pm.ctx, mock.Anything).Return(&fftypes.Blob{PayloadRef: "blob/1"}, nil) - mdx := pm.exchange.(*dataexchangemocks.Plugin) - mdx.On("TransferBLOB", pm.ctx, mock.Anything, "peer1", "blob/1").Return(fmt.Errorf("pop")) mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) + mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + if op.Type != fftypes.OpTypeDataExchangeBlobSend { + return false + } + data := op.Data.(transferBlobData) + return *data.Node.ID == *node.ID + })).Return(fmt.Errorf("pop")) err := pm.transferBlobs(pm.ctx, []*fftypes.Data{ {ID: fftypes.NewUUID(), Hash: fftypes.NewRandB32(), Blob: &fftypes.BlobRef{Hash: fftypes.NewRandB32()}}, - }, fftypes.NewUUID(), &fftypes.Node{ID: fftypes.NewUUID(), DX: fftypes.DXInfo{Peer: "peer1"}}) + }, fftypes.NewUUID(), node) assert.Regexp(t, "pop", err) + + mdi.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferBlobsOpInsertFail(t *testing.T) { From 915bf6e08527fb336f2b6595103f7177f438ba69 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 16 Feb 2022 20:43:15 -0500 Subject: [PATCH 06/10] Add remaining test coverage for new Operations logic Signed-off-by: Andrew Richardson --- internal/assets/operations.go | 2 +- internal/assets/operations_test.go | 424 +++++++++++++++++ internal/batchpin/batchpin_test.go | 5 + internal/batchpin/operations.go | 2 + internal/batchpin/operations_test.go | 146 ++++++ internal/broadcast/manager_test.go | 36 ++ internal/broadcast/operations.go | 2 + internal/broadcast/operations_test.go | 201 ++++++++ internal/contracts/manager.go | 6 +- internal/contracts/operations_test.go | 94 ++++ internal/operations/manager.go | 6 +- internal/operations/manager_test.go | 99 +++- internal/privatemessaging/operations.go | 29 +- internal/privatemessaging/operations_test.go | 454 +++++++++++++++++++ 14 files changed, 1485 insertions(+), 21 deletions(-) create mode 100644 internal/assets/operations_test.go create mode 100644 internal/batchpin/operations_test.go create mode 100644 internal/broadcast/operations_test.go create mode 100644 internal/contracts/operations_test.go create mode 100644 internal/privatemessaging/operations_test.go diff --git a/internal/assets/operations.go b/internal/assets/operations.go index bf8e84075d..4f5aaf521c 100644 --- a/internal/assets/operations.go +++ b/internal/assets/operations.go @@ -96,7 +96,7 @@ func (am *assetManager) RunOperation(ctx context.Context, op *fftypes.PreparedOp return plugin.ActivateTokenPool(ctx, op.ID, data.Pool, data.BlockchainInfo) case transferData: - plugin, err := am.selectTokenPlugin(ctx, data.Transfer.Connector) + plugin, err := am.selectTokenPlugin(ctx, data.Pool.Connector) if err != nil { return false, err } diff --git a/internal/assets/operations_test.go b/internal/assets/operations_test.go new file mode 100644 index 0000000000..c06170f429 --- /dev/null +++ b/internal/assets/operations_test.go @@ -0,0 +1,424 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance 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 imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package assets + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly/internal/txcommon" + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/tokenmocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" +) + +func TestPrepareAndRunCreatePool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenCreatePool, + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + ProtocolID: "F1", + } + err := txcommon.AddTokenPoolCreateInputs(op, pool) + assert.NoError(t, err) + + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mti.On("CreateTokenPool", context.Background(), op.ID, pool).Return(false, nil) + + po, err := am.PrepareOperation(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, pool, po.Data.(createPoolData).Pool) + + complete, err := am.RunOperation(context.Background(), po) + + assert.False(t, complete) + assert.NoError(t, err) + + mti.AssertExpectations(t) +} + +func TestPrepareAndRunActivatePool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenActivatePool, + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + ID: fftypes.NewUUID(), + ProtocolID: "F1", + } + info := fftypes.JSONObject{ + "some": "info", + } + txcommon.AddTokenPoolActivateInputs(op, pool.ID, info) + + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mdi := am.database.(*databasemocks.Plugin) + mti.On("ActivateTokenPool", context.Background(), op.ID, pool, info).Return(true, nil) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(pool, nil) + + po, err := am.PrepareOperation(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, pool, po.Data.(activatePoolData).Pool) + assert.Equal(t, info, po.Data.(activatePoolData).BlockchainInfo) + + complete, err := am.RunOperation(context.Background(), po) + + assert.True(t, complete) + assert.NoError(t, err) + + mti.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestPrepareAndRunTransfer(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenTransfer, + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + ProtocolID: "F1", + } + transfer := &fftypes.TokenTransfer{ + LocalID: fftypes.NewUUID(), + Pool: pool.ID, + Type: fftypes.TokenTransferTypeTransfer, + } + txcommon.AddTokenTransferInputs(op, transfer) + + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mdi := am.database.(*databasemocks.Plugin) + mti.On("TransferTokens", context.Background(), op.ID, "F1", transfer).Return(nil) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(pool, nil) + + po, err := am.PrepareOperation(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, pool, po.Data.(transferData).Pool) + assert.Equal(t, transfer, po.Data.(transferData).Transfer) + + complete, err := am.RunOperation(context.Background(), po) + + assert.False(t, complete) + assert.NoError(t, err) + + mti.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestPrepareOperationNotSupported(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + po, err := am.PrepareOperation(context.Background(), &fftypes.Operation{}) + + assert.Nil(t, po) + assert.Regexp(t, "FF10346", err) +} + +func TestPrepareOperationCreatePoolBadInput(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenCreatePool, + Input: fftypes.JSONObject{"id": "bad"}, + } + + _, err := am.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10151", err) +} + +func TestPrepareOperationActivatePoolBadInput(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenActivatePool, + Input: fftypes.JSONObject{"id": "bad"}, + } + + _, err := am.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10142", err) +} + +func TestPrepareOperationActivatePoolError(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + poolID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenActivatePool, + Input: fftypes.JSONObject{"id": poolID.String()}, + } + + mdi := am.database.(*databasemocks.Plugin) + mdi.On("GetTokenPoolByID", context.Background(), poolID).Return(nil, fmt.Errorf("pop")) + + _, err := am.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationActivatePoolNotFound(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + poolID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenActivatePool, + Input: fftypes.JSONObject{"id": poolID.String()}, + } + + mdi := am.database.(*databasemocks.Plugin) + mdi.On("GetTokenPoolByID", context.Background(), poolID).Return(nil, nil) + + _, err := am.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationTransferBadInput(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenTransfer, + Input: fftypes.JSONObject{"localId": "bad"}, + } + + _, err := am.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10151", err) +} + +func TestPrepareOperationTransferError(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + poolID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenTransfer, + Input: fftypes.JSONObject{"pool": poolID.String()}, + } + + mdi := am.database.(*databasemocks.Plugin) + mdi.On("GetTokenPoolByID", context.Background(), poolID).Return(nil, fmt.Errorf("pop")) + + _, err := am.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationTransferNotFound(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + poolID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenTransfer, + Input: fftypes.JSONObject{"pool": poolID.String()}, + } + + mdi := am.database.(*databasemocks.Plugin) + mdi.On("GetTokenPoolByID", context.Background(), poolID).Return(nil, nil) + + _, err := am.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) +} + +func TestRunOperationNotSupported(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + complete, err := am.RunOperation(context.Background(), &fftypes.PreparedOperation{}) + + assert.False(t, complete) + assert.Regexp(t, "FF10346", err) +} + +func TestRunOperationCreatePoolBadPlugin(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{} + pool := &fftypes.TokenPool{} + + complete, err := am.RunOperation(context.Background(), opCreatePool(op, pool)) + + assert.False(t, complete) + assert.Regexp(t, "FF10272", err) +} + +func TestRunOperationCreatePool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + ID: fftypes.NewUUID(), + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + } + + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mti.On("CreateTokenPool", context.Background(), op.ID, pool).Return(false, nil) + + complete, err := am.RunOperation(context.Background(), opCreatePool(op, pool)) + + assert.False(t, complete) + assert.NoError(t, err) + + mti.AssertExpectations(t) +} + +func TestRunOperationActivatePoolBadPlugin(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{} + pool := &fftypes.TokenPool{} + info := fftypes.JSONObject{} + + complete, err := am.RunOperation(context.Background(), opActivatePool(op, pool, info)) + + assert.False(t, complete) + assert.Regexp(t, "FF10272", err) +} + +func TestRunOperationTransferBadPlugin(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{} + pool := &fftypes.TokenPool{} + transfer := &fftypes.TokenTransfer{} + + complete, err := am.RunOperation(context.Background(), opTransfer(op, pool, transfer)) + + assert.False(t, complete) + assert.Regexp(t, "FF10272", err) +} + +func TestRunOperationTransferUnknownType(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + ID: fftypes.NewUUID(), + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + } + transfer := &fftypes.TokenTransfer{ + Type: "bad", + } + + assert.PanicsWithValue(t, "unknown transfer type: bad", func() { + am.RunOperation(context.Background(), opTransfer(op, pool, transfer)) + }) +} + +func TestRunOperationTransferMint(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + ID: fftypes.NewUUID(), + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + ProtocolID: "F1", + } + transfer := &fftypes.TokenTransfer{ + Type: fftypes.TokenTransferTypeMint, + } + + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mti.On("MintTokens", context.Background(), op.ID, "F1", transfer).Return(nil) + + complete, err := am.RunOperation(context.Background(), opTransfer(op, pool, transfer)) + + assert.False(t, complete) + assert.NoError(t, err) + + mti.AssertExpectations(t) +} + +func TestRunOperationTransferBurn(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + ID: fftypes.NewUUID(), + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + ProtocolID: "F1", + } + transfer := &fftypes.TokenTransfer{ + Type: fftypes.TokenTransferTypeBurn, + } + + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mti.On("BurnTokens", context.Background(), op.ID, "F1", transfer).Return(nil) + + complete, err := am.RunOperation(context.Background(), opTransfer(op, pool, transfer)) + + assert.False(t, complete) + assert.NoError(t, err) + + mti.AssertExpectations(t) +} + +func TestRunOperationTransfer(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + ID: fftypes.NewUUID(), + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + ProtocolID: "F1", + } + transfer := &fftypes.TokenTransfer{ + Type: fftypes.TokenTransferTypeTransfer, + } + + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mti.On("TransferTokens", context.Background(), op.ID, "F1", transfer).Return(nil) + + complete, err := am.RunOperation(context.Background(), opTransfer(op, pool, transfer)) + + assert.False(t, complete) + assert.NoError(t, err) + + mti.AssertExpectations(t) +} diff --git a/internal/batchpin/batchpin_test.go b/internal/batchpin/batchpin_test.go index 51d778fbd8..a5c88b082d 100644 --- a/internal/batchpin/batchpin_test.go +++ b/internal/batchpin/batchpin_test.go @@ -51,6 +51,11 @@ func newTestBatchPinSubmitter(t *testing.T, enableMetrics bool) *batchPinSubmitt return bps.(*batchPinSubmitter) } +func TestInitFail(t *testing.T) { + _, err := NewBatchPinSubmitter(context.Background(), nil, nil, nil, nil, nil) + assert.Regexp(t, "FF10128", err) +} + func TestSubmitPinnedBatchOk(t *testing.T) { bp := newTestBatchPinSubmitter(t, false) ctx := context.Background() diff --git a/internal/batchpin/operations.go b/internal/batchpin/operations.go index fa39d2f872..80b481b868 100644 --- a/internal/batchpin/operations.go +++ b/internal/batchpin/operations.go @@ -66,6 +66,8 @@ func (bp *batchPinSubmitter) PrepareOperation(ctx context.Context, op *fftypes.O batch, err := bp.database.GetBatchByID(ctx, batchID) if err != nil { return nil, err + } else if batch == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) } return opBatchPin(op, batch, contexts), nil diff --git a/internal/batchpin/operations_test.go b/internal/batchpin/operations_test.go new file mode 100644 index 0000000000..147756e120 --- /dev/null +++ b/internal/batchpin/operations_test.go @@ -0,0 +1,146 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance 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 imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package batchpin + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly/mocks/blockchainmocks" + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPrepareAndRunBatchPin(t *testing.T) { + bp := newTestBatchPinSubmitter(t, false) + + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainBatchPin, + ID: fftypes.NewUUID(), + } + batch := &fftypes.Batch{ + ID: fftypes.NewUUID(), + Identity: fftypes.Identity{ + Key: "0x123", + }, + } + contexts := []*fftypes.Bytes32{ + fftypes.NewRandB32(), + fftypes.NewRandB32(), + } + addBatchPinInputs(op, batch.ID, contexts) + + mbi := bp.blockchain.(*blockchainmocks.Plugin) + mdi := bp.database.(*databasemocks.Plugin) + mdi.On("GetBatchByID", context.Background(), batch.ID).Return(batch, nil) + mbi.On("SubmitBatchPin", context.Background(), op.ID, mock.Anything, "0x123", mock.Anything).Return(nil) + + po, err := bp.PrepareOperation(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, batch, po.Data.(batchPinData).Batch) + + complete, err := bp.RunOperation(context.Background(), opBatchPin(op, batch, contexts)) + + assert.False(t, complete) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationNotSupported(t *testing.T) { + bp := newTestBatchPinSubmitter(t, false) + + po, err := bp.PrepareOperation(context.Background(), &fftypes.Operation{}) + + assert.Nil(t, po) + assert.Regexp(t, "FF10346", err) +} + +func TestPrepareOperationBatchPinBadBatch(t *testing.T) { + bp := newTestBatchPinSubmitter(t, false) + + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{"batch": "bad"}, + } + + _, err := bp.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10142", err) +} + +func TestPrepareOperationBatchPinBadContext(t *testing.T) { + bp := newTestBatchPinSubmitter(t, false) + + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{ + "batch": fftypes.NewUUID().String(), + "contexts": []string{"bad"}, + }, + } + + _, err := bp.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10232", err) +} + +func TestRunOperationNotSupported(t *testing.T) { + bp := newTestBatchPinSubmitter(t, false) + + complete, err := bp.RunOperation(context.Background(), &fftypes.PreparedOperation{}) + + assert.False(t, complete) + assert.Regexp(t, "FF10346", err) +} + +func TestPrepareOperationBatchPinError(t *testing.T) { + bp := newTestBatchPinSubmitter(t, false) + + batchID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{ + "batch": batchID.String(), + "contexts": []string{}, + }, + } + + mdi := bp.database.(*databasemocks.Plugin) + mdi.On("GetBatchByID", context.Background(), batchID).Return(nil, fmt.Errorf("pop")) + + _, err := bp.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") +} + +func TestPrepareOperationBatchPinNotFound(t *testing.T) { + bp := newTestBatchPinSubmitter(t, false) + + batchID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{ + "batch": batchID.String(), + "contexts": []string{}, + }, + } + + mdi := bp.database.(*databasemocks.Plugin) + mdi.On("GetBatchByID", context.Background(), batchID).Return(nil, nil) + + _, err := bp.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) +} diff --git a/internal/broadcast/manager_test.go b/internal/broadcast/manager_test.go index bbf1c9373d..33e6bb8406 100644 --- a/internal/broadcast/manager_test.go +++ b/internal/broadcast/manager_test.go @@ -138,6 +138,42 @@ func TestBroadcastMessageBad(t *testing.T) { } +func TestDispatchBatchInsertOpFail(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + batch := &fftypes.Batch{} + + mdi := bm.database.(*databasemocks.Plugin) + mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + + err := bm.dispatchBatch(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestDispatchBatchUploadFail(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + batch := &fftypes.Batch{} + + mdi := bm.database.(*databasemocks.Plugin) + mom := bm.operations.(*operationmocks.Manager) + mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) + mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(batchBroadcastData) + return op.Type == fftypes.OpTypePublicStorageBatchBroadcast && data.Batch == batch + })).Return(fmt.Errorf("pop")) + + err := bm.dispatchBatch(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) + mom.AssertExpectations(t) +} + func TestDispatchBatchSubmitBatchPinSucceed(t *testing.T) { bm, cancel := newTestBroadcast(t) defer cancel() diff --git a/internal/broadcast/operations.go b/internal/broadcast/operations.go index 8b9c718b9c..f15b3e9351 100644 --- a/internal/broadcast/operations.go +++ b/internal/broadcast/operations.go @@ -50,6 +50,8 @@ func (bm *broadcastManager) PrepareOperation(ctx context.Context, op *fftypes.Op batch, err := bm.database.GetBatchByID(ctx, id) if err != nil { return nil, err + } else if batch == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) } return opBatchBroadcast(op, batch), nil diff --git a/internal/broadcast/operations_test.go b/internal/broadcast/operations_test.go new file mode 100644 index 0000000000..4564125ff3 --- /dev/null +++ b/internal/broadcast/operations_test.go @@ -0,0 +1,201 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance 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 imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package broadcast + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/publicstoragemocks" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPrepareAndRunBatchBroadcast(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypePublicStorageBatchBroadcast, + } + batch := &fftypes.Batch{ + ID: fftypes.NewUUID(), + } + addBatchBroadcastInputs(op, batch.ID) + + mps := bm.publicstorage.(*publicstoragemocks.Plugin) + mdi := bm.database.(*databasemocks.Plugin) + mdi.On("GetBatchByID", context.Background(), batch.ID).Return(batch, nil) + mps.On("PublishData", context.Background(), mock.Anything).Return("123", nil) + mdi.On("UpdateBatch", context.Background(), batch.ID, mock.MatchedBy(func(update database.Update) bool { + info, _ := update.Finalize() + assert.Equal(t, 1, len(info.SetOperations)) + assert.Equal(t, "payloadref", info.SetOperations[0].Field) + val, _ := info.SetOperations[0].Value.Value() + assert.Equal(t, "123", val) + return true + })).Return(nil) + + po, err := bm.PrepareOperation(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, batch, po.Data.(batchBroadcastData).Batch) + + complete, err := bm.RunOperation(context.Background(), opBatchBroadcast(op, batch)) + + assert.True(t, complete) + assert.NoError(t, err) + + mps.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestPrepareOperationNotSupported(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + _, err := bm.PrepareOperation(context.Background(), &fftypes.Operation{}) + + assert.Regexp(t, "FF10346", err) +} + +func TestPrepareOperationBatchBroadcastBadInput(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypePublicStorageBatchBroadcast, + Input: fftypes.JSONObject{"id": "bad"}, + } + + _, err := bm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10142", err) +} + +func TestPrepareOperationBatchBroadcastError(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + batchID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypePublicStorageBatchBroadcast, + Input: fftypes.JSONObject{"id": batchID.String()}, + } + + mdi := bm.database.(*databasemocks.Plugin) + mdi.On("GetBatchByID", context.Background(), batchID).Return(nil, fmt.Errorf("pop")) + + _, err := bm.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") +} + +func TestPrepareOperationBatchBroadcastNotFound(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + batchID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypePublicStorageBatchBroadcast, + Input: fftypes.JSONObject{"id": batchID.String()}, + } + + mdi := bm.database.(*databasemocks.Plugin) + mdi.On("GetBatchByID", context.Background(), batchID).Return(nil, nil) + + _, err := bm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) +} + +func TestRunOperationNotSupported(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + complete, err := bm.RunOperation(context.Background(), &fftypes.PreparedOperation{}) + + assert.False(t, complete) + assert.Regexp(t, "FF10346", err) +} + +func TestRunOperationBatchBroadcastInvalidData(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + op := &fftypes.Operation{} + batch := &fftypes.Batch{ + Payload: fftypes.BatchPayload{ + Data: []*fftypes.Data{ + {Value: fftypes.JSONAnyPtr(`!json`)}, + }, + }, + } + + complete, err := bm.RunOperation(context.Background(), opBatchBroadcast(op, batch)) + + assert.False(t, complete) + assert.Regexp(t, "FF10137", err) +} + +func TestRunOperationBatchBroadcastPublishFail(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + op := &fftypes.Operation{} + batch := &fftypes.Batch{ + ID: fftypes.NewUUID(), + } + + mps := bm.publicstorage.(*publicstoragemocks.Plugin) + mps.On("PublishData", context.Background(), mock.Anything).Return("", fmt.Errorf("pop")) + + complete, err := bm.RunOperation(context.Background(), opBatchBroadcast(op, batch)) + + assert.False(t, complete) + assert.EqualError(t, err, "pop") + + mps.AssertExpectations(t) +} + +func TestRunOperationBatchBroadcast(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + op := &fftypes.Operation{} + batch := &fftypes.Batch{ + ID: fftypes.NewUUID(), + } + + mps := bm.publicstorage.(*publicstoragemocks.Plugin) + mdi := bm.database.(*databasemocks.Plugin) + mps.On("PublishData", context.Background(), mock.Anything).Return("123", nil) + mdi.On("UpdateBatch", context.Background(), batch.ID, mock.MatchedBy(func(update database.Update) bool { + info, _ := update.Finalize() + assert.Equal(t, 1, len(info.SetOperations)) + assert.Equal(t, "payloadref", info.SetOperations[0].Field) + val, _ := info.SetOperations[0].Value.Value() + assert.Equal(t, "123", val) + return true + })).Return(nil) + + complete, err := bm.RunOperation(context.Background(), opBatchBroadcast(op, batch)) + + assert.True(t, complete) + assert.NoError(t, err) + + mps.AssertExpectations(t) + mdi.AssertExpectations(t) +} diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index 517890c702..1b04a4aa84 100644 --- a/internal/contracts/manager.go +++ b/internal/contracts/manager.go @@ -185,10 +185,10 @@ func (cm *contractManager) writeInvokeTransaction(ctx context.Context, ns string ns, txid, fftypes.OpTypeBlockchainInvoke) - if err := addBlockchainInvokeInputs(op, req); err != nil { - return nil, err + if err = addBlockchainInvokeInputs(op, req); err == nil { + err = cm.database.InsertOperation(ctx, op) } - return op, cm.database.InsertOperation(ctx, op) + return op, err } func (cm *contractManager) InvokeContract(ctx context.Context, ns string, req *fftypes.ContractCallRequest) (res interface{}, err error) { diff --git a/internal/contracts/operations_test.go b/internal/contracts/operations_test.go new file mode 100644 index 0000000000..d0abed58c0 --- /dev/null +++ b/internal/contracts/operations_test.go @@ -0,0 +1,94 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance 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 imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package contracts + +import ( + "context" + "testing" + + "github.com/hyperledger/firefly/mocks/blockchainmocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPrepareAndRunBlockchainInvoke(t *testing.T) { + cm := newTestContractManager() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainInvoke, + ID: fftypes.NewUUID(), + } + req := &fftypes.ContractCallRequest{ + Key: "0x123", + Location: fftypes.JSONAnyPtr(`{"address":"0x1111"}`), + Method: &fftypes.FFIMethod{ + Name: "set", + }, + Input: map[string]interface{}{ + "value": "1", + }, + } + err := addBlockchainInvokeInputs(op, req) + assert.NoError(t, err) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("InvokeContract", context.Background(), op.ID, "0x123", mock.MatchedBy(func(loc *fftypes.JSONAny) bool { + return loc.String() == req.Location.String() + }), mock.MatchedBy(func(method *fftypes.FFIMethod) bool { + return method.Name == req.Method.Name + }), req.Input).Return(nil) + + po, err := cm.PrepareOperation(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, req, po.Data.(blockchainInvokeData).Request) + + complete, err := cm.RunOperation(context.Background(), po) + + assert.False(t, complete) + assert.NoError(t, err) + + mbi.AssertExpectations(t) +} + +func TestPrepareOperationNotSupported(t *testing.T) { + cm := newTestContractManager() + + po, err := cm.PrepareOperation(context.Background(), &fftypes.Operation{}) + + assert.Nil(t, po) + assert.Regexp(t, "FF10346", err) +} + +func TestPrepareOperationBlockchainInvokeBadInput(t *testing.T) { + cm := newTestContractManager() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainInvoke, + Input: fftypes.JSONObject{"interface": "bad"}, + } + + _, err := cm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10151", err) +} + +func TestRunOperationNotSupported(t *testing.T) { + cm := newTestContractManager() + + complete, err := cm.RunOperation(context.Background(), &fftypes.PreparedOperation{}) + + assert.False(t, complete) + assert.Regexp(t, "FF10346", err) +} diff --git a/internal/operations/manager.go b/internal/operations/manager.go index ea6d5a4007..4294e88590 100644 --- a/internal/operations/manager.go +++ b/internal/operations/manager.go @@ -80,13 +80,13 @@ func (om *operationsManager) RunOperation(ctx context.Context, op *fftypes.Prepa om.writeOperationFailure(ctx, op.ID, err) return err } else if complete { - om.writeOperationSuccess(ctx, op.ID, nil) + om.writeOperationSuccess(ctx, op.ID) } return nil } -func (om *operationsManager) writeOperationSuccess(ctx context.Context, opID *fftypes.UUID, output fftypes.JSONObject) { - if err := om.database.ResolveOperation(ctx, opID, fftypes.OpStatusSucceeded, "", output); err != nil { +func (om *operationsManager) writeOperationSuccess(ctx context.Context, opID *fftypes.UUID) { + if err := om.database.ResolveOperation(ctx, opID, fftypes.OpStatusSucceeded, "", nil); err != nil { log.L(ctx).Errorf("Failed to update operation %s: %s", opID, err) } } diff --git a/internal/operations/manager_test.go b/internal/operations/manager_test.go index 19dfc52037..89df9c3278 100644 --- a/internal/operations/manager_test.go +++ b/internal/operations/manager_test.go @@ -28,6 +28,19 @@ import ( "github.com/stretchr/testify/mock" ) +type mockHandler struct { + Complete bool + Err error +} + +func (m *mockHandler) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + return nil, m.Err +} + +func (m *mockHandler) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) { + return m.Complete, m.Err +} + func newTestOperations(t *testing.T) (*operationsManager, func()) { config.Reset() mdi := &databasemocks.Plugin{} @@ -39,6 +52,11 @@ func newTestOperations(t *testing.T) (*operationsManager, func()) { return om.(*operationsManager), cancel } +func TestInitFail(t *testing.T) { + _, err := NewOperationsManager(context.Background(), nil, nil) + assert.Regexp(t, "FF10128", err) +} + func TestPrepareOperationNotSupported(t *testing.T) { om, cancel := newTestOperations(t) defer cancel() @@ -49,18 +67,93 @@ func TestPrepareOperationNotSupported(t *testing.T) { assert.Regexp(t, "FF10346", err) } +func TestPrepareOperationSuccess(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainBatchPin, + } + + om.RegisterHandler(&mockHandler{}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + _, err := om.PrepareOperation(context.Background(), op) + + assert.NoError(t, err) +} + +func TestRunOperationNotSupported(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + op := &fftypes.PreparedOperation{} + + err := om.RunOperation(context.Background(), op) + assert.Regexp(t, "FF10346", err) +} + +func TestRunOperationSuccess(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + op := &fftypes.PreparedOperation{ + Type: fftypes.OpTypeBlockchainBatchPin, + } + + om.RegisterHandler(&mockHandler{}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + err := om.RunOperation(context.Background(), op) + + assert.NoError(t, err) +} + +func TestRunOperationSyncSuccess(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + op := &fftypes.PreparedOperation{ + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("ResolveOperation", ctx, op.ID, fftypes.OpStatusSucceeded, "", mock.Anything).Return(nil) + + om.RegisterHandler(&mockHandler{Complete: true}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + err := om.RunOperation(context.Background(), op) + + assert.NoError(t, err) +} + +func TestRunOperationFail(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + op := &fftypes.PreparedOperation{ + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("ResolveOperation", ctx, op.ID, fftypes.OpStatusFailed, "pop", mock.Anything).Return(nil) + + om.RegisterHandler(&mockHandler{Err: fmt.Errorf("pop")}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + err := om.RunOperation(context.Background(), op) + + assert.EqualError(t, err, "pop") +} + func TestWriteOperationSuccess(t *testing.T) { om, cancel := newTestOperations(t) defer cancel() ctx := context.Background() opID := fftypes.NewUUID() - output := fftypes.JSONObject{"some": "info"} mdi := om.database.(*databasemocks.Plugin) - mdi.On("ResolveOperation", ctx, opID, fftypes.OpStatusSucceeded, "", output).Return(fmt.Errorf("pop")) + mdi.On("ResolveOperation", ctx, opID, fftypes.OpStatusSucceeded, "", mock.Anything).Return(fmt.Errorf("pop")) - om.writeOperationSuccess(ctx, opID, output) + om.writeOperationSuccess(ctx, opID) mdi.AssertExpectations(t) diff --git a/internal/privatemessaging/operations.go b/internal/privatemessaging/operations.go index 1f0789292b..3252e6a219 100644 --- a/internal/privatemessaging/operations.go +++ b/internal/privatemessaging/operations.go @@ -60,24 +60,21 @@ func addBatchSendInputs(op *fftypes.Operation, nodeID *fftypes.UUID, groupHash * func retrieveBatchSendInputs(ctx context.Context, op *fftypes.Operation) (nodeID *fftypes.UUID, groupHash *fftypes.Bytes32, batchID *fftypes.UUID, manifest string, err error) { nodeID, err = fftypes.ParseUUID(ctx, op.Input.GetString("node")) - if err != nil { - return nil, nil, nil, "", err + if err == nil { + groupHash, err = fftypes.ParseBytes32(ctx, op.Input.GetString("group")) } - groupHash, err = fftypes.ParseBytes32(ctx, op.Input.GetString("group")) - if err != nil { - return nil, nil, nil, "", err + if err == nil { + batchID, err = fftypes.ParseUUID(ctx, op.Input.GetString("batch")) } - batchID, err = fftypes.ParseUUID(ctx, op.Input.GetString("batch")) - if err != nil { - return nil, nil, nil, "", err + if err == nil { + manifest = op.Input.GetString("manifest") } - manifest = op.Input.GetString("manifest") - return nodeID, groupHash, batchID, manifest, nil + return nodeID, groupHash, batchID, manifest, err } func (pm *privateMessaging) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { switch op.Type { - case fftypes.OpTypePublicStorageBatchBroadcast: + case fftypes.OpTypeDataExchangeBlobSend: nodeID, blobHash, err := retrieveTransferBlobInputs(ctx, op) if err != nil { return nil, err @@ -85,10 +82,14 @@ func (pm *privateMessaging) PrepareOperation(ctx context.Context, op *fftypes.Op node, err := pm.database.GetNodeByID(ctx, nodeID) if err != nil { return nil, err + } else if node == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) } blob, err := pm.database.GetBlobMatchingHash(ctx, blobHash) if err != nil { return nil, err + } else if blob == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) } return opTransferBlob(op, node, blob), nil @@ -100,14 +101,20 @@ func (pm *privateMessaging) PrepareOperation(ctx context.Context, op *fftypes.Op node, err := pm.database.GetNodeByID(ctx, nodeID) if err != nil { return nil, err + } else if node == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) } group, err := pm.database.GetGroupByHash(ctx, groupHash) if err != nil { return nil, err + } else if group == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) } batch, err := pm.database.GetBatchByID(ctx, batchID) if err != nil { return nil, err + } else if batch == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) } transport := &fftypes.TransportWrapper{Group: group, Batch: batch} return opBatchSend(op, node, transport), nil diff --git a/internal/privatemessaging/operations_test.go b/internal/privatemessaging/operations_test.go new file mode 100644 index 0000000000..09509d25cf --- /dev/null +++ b/internal/privatemessaging/operations_test.go @@ -0,0 +1,454 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance 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 imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package privatemessaging + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/dataexchangemocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPrepareAndRunTransferBlob(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBlobSend, + ID: fftypes.NewUUID(), + } + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + DX: fftypes.DXInfo{ + Peer: "peer1", + }, + } + blob := &fftypes.Blob{ + Hash: fftypes.NewRandB32(), + PayloadRef: "payload", + } + addTransferBlobInputs(op, node.ID, blob.Hash) + + mdi := pm.database.(*databasemocks.Plugin) + mdx := pm.exchange.(*dataexchangemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), node.ID).Return(node, nil) + mdi.On("GetBlobMatchingHash", context.Background(), blob.Hash).Return(blob, nil) + mdx.On("TransferBLOB", context.Background(), op.ID, "peer1", "payload").Return(nil) + + po, err := pm.PrepareOperation(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, node, po.Data.(transferBlobData).Node) + assert.Equal(t, blob, po.Data.(transferBlobData).Blob) + + complete, err := pm.RunOperation(context.Background(), po) + + assert.False(t, complete) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdx.AssertExpectations(t) +} + +func TestPrepareAndRunBatchSend(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBatchSend, + ID: fftypes.NewUUID(), + } + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + DX: fftypes.DXInfo{ + Peer: "peer1", + }, + } + group := &fftypes.Group{ + Hash: fftypes.NewRandB32(), + } + batch := &fftypes.Batch{ + ID: fftypes.NewUUID(), + } + addBatchSendInputs(op, node.ID, group.Hash, batch.ID, "manifest-info") + + mdi := pm.database.(*databasemocks.Plugin) + mdx := pm.exchange.(*dataexchangemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), node.ID).Return(node, nil) + mdi.On("GetGroupByHash", context.Background(), group.Hash).Return(group, nil) + mdi.On("GetBatchByID", context.Background(), batch.ID).Return(batch, nil) + mdx.On("SendMessage", context.Background(), op.ID, "peer1", mock.Anything).Return(nil) + + po, err := pm.PrepareOperation(context.Background(), op) + assert.NoError(t, err) + assert.Equal(t, node, po.Data.(batchSendData).Node) + assert.Equal(t, group, po.Data.(batchSendData).Transport.Group) + assert.Equal(t, batch, po.Data.(batchSendData).Transport.Batch) + + complete, err := pm.RunOperation(context.Background(), po) + + assert.False(t, complete) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdx.AssertExpectations(t) +} + +func TestPrepareOperationNotSupported(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + po, err := pm.PrepareOperation(context.Background(), &fftypes.Operation{}) + + assert.Nil(t, po) + assert.Regexp(t, "FF10346", err) +} + +func TestPrepareOperationBlobSendBadInput(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBlobSend, + Input: fftypes.JSONObject{"node": "bad"}, + } + + _, err := pm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10142", err) +} + +func TestPrepareOperationBlobSendNodeFail(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + nodeID := fftypes.NewUUID() + blobHash := fftypes.NewRandB32() + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBlobSend, + Input: fftypes.JSONObject{ + "node": nodeID.String(), + "blob": blobHash.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), nodeID).Return(nil, fmt.Errorf("pop")) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBlobSendNodeNotFound(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + nodeID := fftypes.NewUUID() + blobHash := fftypes.NewRandB32() + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBlobSend, + Input: fftypes.JSONObject{ + "node": nodeID.String(), + "blob": blobHash.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), nodeID).Return(nil, nil) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBlobSendBlobFail(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + blobHash := fftypes.NewRandB32() + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + DX: fftypes.DXInfo{ + Peer: "peer1", + }, + } + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBlobSend, + Input: fftypes.JSONObject{ + "node": node.ID.String(), + "blob": blobHash.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), node.ID).Return(node, nil) + mdi.On("GetBlobMatchingHash", context.Background(), blobHash).Return(nil, fmt.Errorf("pop")) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBlobSendBlobNotFound(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + blobHash := fftypes.NewRandB32() + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + DX: fftypes.DXInfo{ + Peer: "peer1", + }, + } + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBlobSend, + Input: fftypes.JSONObject{ + "node": node.ID.String(), + "blob": blobHash.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), node.ID).Return(node, nil) + mdi.On("GetBlobMatchingHash", context.Background(), blobHash).Return(nil, nil) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBatchSendBadInput(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBatchSend, + Input: fftypes.JSONObject{"node": "bad"}, + } + + _, err := pm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10142", err) +} + +func TestPrepareOperationBatchSendNodeFail(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + nodeID := fftypes.NewUUID() + groupHash := fftypes.NewRandB32() + batchID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBatchSend, + Input: fftypes.JSONObject{ + "node": nodeID.String(), + "group": groupHash.String(), + "batch": batchID.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), nodeID).Return(nil, fmt.Errorf("pop")) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBatchSendNodeNotFound(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + nodeID := fftypes.NewUUID() + groupHash := fftypes.NewRandB32() + batchID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBatchSend, + Input: fftypes.JSONObject{ + "node": nodeID.String(), + "group": groupHash.String(), + "batch": batchID.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), nodeID).Return(nil, nil) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBatchSendGroupFail(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + groupHash := fftypes.NewRandB32() + batchID := fftypes.NewUUID() + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + } + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBatchSend, + Input: fftypes.JSONObject{ + "node": node.ID.String(), + "group": groupHash.String(), + "batch": batchID.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), node.ID).Return(node, nil) + mdi.On("GetGroupByHash", context.Background(), groupHash).Return(nil, fmt.Errorf("pop")) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBatchSendGroupNotFound(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + groupHash := fftypes.NewRandB32() + batchID := fftypes.NewUUID() + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + } + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBatchSend, + Input: fftypes.JSONObject{ + "node": node.ID.String(), + "group": groupHash.String(), + "batch": batchID.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), node.ID).Return(node, nil) + mdi.On("GetGroupByHash", context.Background(), groupHash).Return(nil, nil) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBatchSendBatchFail(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + batchID := fftypes.NewUUID() + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + } + group := &fftypes.Group{ + Hash: fftypes.NewRandB32(), + } + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBatchSend, + Input: fftypes.JSONObject{ + "node": node.ID.String(), + "group": group.Hash.String(), + "batch": batchID.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), node.ID).Return(node, nil) + mdi.On("GetGroupByHash", context.Background(), group.Hash).Return(group, nil) + mdi.On("GetBatchByID", context.Background(), batchID).Return(nil, fmt.Errorf("pop")) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestPrepareOperationBatchSendBatchNotFound(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + batchID := fftypes.NewUUID() + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + } + group := &fftypes.Group{ + Hash: fftypes.NewRandB32(), + } + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBatchSend, + Input: fftypes.JSONObject{ + "node": node.ID.String(), + "group": group.Hash.String(), + "batch": batchID.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetNodeByID", context.Background(), node.ID).Return(node, nil) + mdi.On("GetGroupByHash", context.Background(), group.Hash).Return(group, nil) + mdi.On("GetBatchByID", context.Background(), batchID).Return(nil, nil) + + _, err := pm.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) +} + +func TestRunOperationNotSupported(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + complete, err := pm.RunOperation(context.Background(), &fftypes.PreparedOperation{}) + + assert.False(t, complete) + assert.Regexp(t, "FF10346", err) +} + +func TestRunOperationBatchSendInvalidData(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + op := &fftypes.Operation{} + node := &fftypes.Node{ + ID: fftypes.NewUUID(), + } + transport := &fftypes.TransportWrapper{ + Group: &fftypes.Group{}, + Batch: &fftypes.Batch{ + Payload: fftypes.BatchPayload{ + Data: []*fftypes.Data{ + {Value: fftypes.JSONAnyPtr(`!json`)}, + }, + }, + }, + } + + complete, err := pm.RunOperation(context.Background(), opBatchSend(op, node, transport)) + + assert.False(t, complete) + assert.Regexp(t, "FF10137", err) +} From 52ced7cb6ac702d58351827b8b374144655af5dd Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 17 Feb 2022 15:38:03 -0500 Subject: [PATCH 07/10] Add route for operation retry Signed-off-by: Andrew Richardson --- .../000061_add_operation_retry.down.sql | 3 + .../000061_add_operation_retry.up.sql | 3 + .../000061_add_operation_retry.down.sql | 1 + .../sqlite/000061_add_operation_retry.up.sql | 1 + docs/swagger/swagger.yaml | 79 +++++++++ internal/apiserver/route_post_op_retry.go | 52 ++++++ .../apiserver/route_post_op_retry_test.go | 62 +++++++ internal/apiserver/routes.go | 1 + internal/database/sqlcommon/operation_sql.go | 8 +- .../database/sqlcommon/operation_sql_test.go | 6 +- internal/operations/manager.go | 35 ++++ internal/operations/manager_test.go | 155 +++++++++++++++++- internal/orchestrator/orchestrator.go | 5 + internal/orchestrator/orchestrator_test.go | 1 + mocks/databasemocks/plugin.go | 14 ++ mocks/operationmocks/manager.go | 23 +++ mocks/orchestratormocks/orchestrator.go | 18 ++ mocks/txcommonmocks/helper.go | 10 -- pkg/database/plugin.go | 4 + pkg/fftypes/operation.go | 1 + 20 files changed, 462 insertions(+), 20 deletions(-) create mode 100644 db/migrations/postgres/000061_add_operation_retry.down.sql create mode 100644 db/migrations/postgres/000061_add_operation_retry.up.sql create mode 100644 db/migrations/sqlite/000061_add_operation_retry.down.sql create mode 100644 db/migrations/sqlite/000061_add_operation_retry.up.sql create mode 100644 internal/apiserver/route_post_op_retry.go create mode 100644 internal/apiserver/route_post_op_retry_test.go diff --git a/db/migrations/postgres/000061_add_operation_retry.down.sql b/db/migrations/postgres/000061_add_operation_retry.down.sql new file mode 100644 index 0000000000..e315703994 --- /dev/null +++ b/db/migrations/postgres/000061_add_operation_retry.down.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE operations DROP COLUMN retry_id; +COMMIT; diff --git a/db/migrations/postgres/000061_add_operation_retry.up.sql b/db/migrations/postgres/000061_add_operation_retry.up.sql new file mode 100644 index 0000000000..8c3db8d2a3 --- /dev/null +++ b/db/migrations/postgres/000061_add_operation_retry.up.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE operations ADD COLUMN retry_id UUID; +COMMIT; diff --git a/db/migrations/sqlite/000061_add_operation_retry.down.sql b/db/migrations/sqlite/000061_add_operation_retry.down.sql new file mode 100644 index 0000000000..0415eac239 --- /dev/null +++ b/db/migrations/sqlite/000061_add_operation_retry.down.sql @@ -0,0 +1 @@ +ALTER TABLE operations DROP COLUMN retry_id; diff --git a/db/migrations/sqlite/000061_add_operation_retry.up.sql b/db/migrations/sqlite/000061_add_operation_retry.up.sql new file mode 100644 index 0000000000..16668cf665 --- /dev/null +++ b/db/migrations/sqlite/000061_add_operation_retry.up.sql @@ -0,0 +1 @@ +ALTER TABLE operations ADD COLUMN retry_id UUID; diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index ef9603da08..5f9fcf4eb7 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -4770,6 +4770,7 @@ paths: type: object plugin: type: string + retry: {} status: type: string tx: {} @@ -5457,6 +5458,11 @@ paths: name: plugin schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: retry + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: status @@ -5531,6 +5537,7 @@ paths: type: object plugin: type: string + retry: {} status: type: string tx: {} @@ -5595,6 +5602,77 @@ paths: type: object plugin: type: string + retry: {} + status: + type: string + tx: {} + type: + enum: + - blockchain_batch_pin + - blockchain_invoke + - publicstorage_batch_broadcast + - dataexchange_batch_send + - dataexchange_blob_send + - token_create_pool + - token_activate_pool + - token_transfer + type: string + updated: {} + type: object + description: Success + default: + description: "" + /namespaces/{ns}/operations/{opid}/retry: + post: + description: 'TODO: Description' + operationId: postOpRetry + parameters: + - description: 'TODO: Description' + in: path + name: ns + required: true + schema: + example: default + type: string + - description: 'TODO: Description' + in: path + name: opid + required: true + schema: + type: string + - description: Server-side request timeout (millseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 120s + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "202": + content: + application/json: + schema: + properties: + created: {} + error: + type: string + id: {} + input: + additionalProperties: {} + type: object + namespace: + type: string + output: + additionalProperties: {} + type: object + plugin: + type: string + retry: {} status: type: string tx: {} @@ -8172,6 +8250,7 @@ paths: type: object plugin: type: string + retry: {} status: type: string tx: {} diff --git a/internal/apiserver/route_post_op_retry.go b/internal/apiserver/route_post_op_retry.go new file mode 100644 index 0000000000..18e778a43d --- /dev/null +++ b/internal/apiserver/route_post_op_retry.go @@ -0,0 +1,52 @@ +// Copyright © 2022 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 apiserver + +import ( + "context" + "net/http" + + "github.com/hyperledger/firefly/internal/config" + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/internal/oapispec" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +var postOpRetry = &oapispec.Route{ + Name: "postOpRetry", + Path: "namespaces/{ns}/operations/{opid}/retry", + Method: http.MethodPost, + PathParams: []*oapispec.PathParam{ + {Name: "ns", ExampleFromConf: config.NamespacesDefault, Description: i18n.MsgTBD}, + {Name: "opid", Description: i18n.MsgTBD}, + }, + QueryParams: []*oapispec.QueryParam{}, + FilterFactory: nil, + Description: i18n.MsgTBD, + JSONInputValue: func() interface{} { return &fftypes.EmptyInput{} }, + JSONInputMask: nil, + JSONInputSchema: func(ctx context.Context) string { return emptyObjectSchema }, + JSONOutputValue: func() interface{} { return &fftypes.Operation{} }, + JSONOutputCodes: []int{http.StatusAccepted}, + JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { + opid, err := fftypes.ParseUUID(r.Ctx, r.PP["opid"]) + if err != nil { + return nil, err + } + return getOr(r.Ctx).Operations().RetryOperation(r.Ctx, r.PP["ns"], opid) + }, +} diff --git a/internal/apiserver/route_post_op_retry_test.go b/internal/apiserver/route_post_op_retry_test.go new file mode 100644 index 0000000000..2094a3c8ff --- /dev/null +++ b/internal/apiserver/route_post_op_retry_test.go @@ -0,0 +1,62 @@ +// Copyright © 2021 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 apiserver + +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/hyperledger/firefly/mocks/operationmocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostOpRetry(t *testing.T) { + o, r := newTestAPIServer() + mom := &operationmocks.Manager{} + o.On("Operations").Return(mom) + input := fftypes.EmptyInput{} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + opID := fftypes.NewUUID() + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/operations/"+opID.String()+"/retry", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + mom.On("RetryOperation", mock.Anything, "ns1", opID). + Return(&fftypes.Operation{}, nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 202, res.Result().StatusCode) +} + +func TestPostOpRetryBadID(t *testing.T) { + _, r := newTestAPIServer() + input := fftypes.EmptyInput{} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/operations/bad/retry", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + r.ServeHTTP(res, req) + + assert.Equal(t, 400, res.Result().StatusCode) +} diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index 8bfc5e117f..ef319bc8a8 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -63,6 +63,7 @@ var routes = []*oapispec.Route{ getNamespaces, getOpByID, getOps, + postOpRetry, getStatus, getStatusBatchManager, getSubscriptionByID, diff --git a/internal/database/sqlcommon/operation_sql.go b/internal/database/sqlcommon/operation_sql.go index cd148792d1..b2742b45dc 100644 --- a/internal/database/sqlcommon/operation_sql.go +++ b/internal/database/sqlcommon/operation_sql.go @@ -40,11 +40,13 @@ var ( "error", "input", "output", + "retry_id", } opFilterFieldMap = map[string]string{ "tx": "tx_id", "type": "optype", "status": "opstatus", + "retry": "retry_id", } ) @@ -70,6 +72,7 @@ func (s *SQLCommon) InsertOperation(ctx context.Context, operation *fftypes.Oper operation.Error, operation.Input, operation.Output, + operation.Retry, ), func() { s.callbacks.UUIDCollectionNSEvent(database.CollectionOperations, fftypes.ChangeEventTypeCreated, operation.Namespace, operation.ID) @@ -95,6 +98,7 @@ func (s *SQLCommon) opResult(ctx context.Context, row *sql.Rows) (*fftypes.Opera &op.Error, &op.Input, &op.Output, + &op.Retry, ) if err != nil { return nil, i18n.WrapError(ctx, err, i18n.MsgDBReadErr, "operations") @@ -152,7 +156,7 @@ func (s *SQLCommon) GetOperations(ctx context.Context, filter database.Filter) ( return ops, s.queryRes(ctx, tx, "operations", fop, fi), err } -func (s *SQLCommon) updateOperation(ctx context.Context, id *fftypes.UUID, update database.Update) (err error) { +func (s *SQLCommon) UpdateOperation(ctx context.Context, id *fftypes.UUID, update database.Update) (err error) { ctx, tx, autoCommit, err := s.beginOrUseTx(ctx) if err != nil { @@ -182,5 +186,5 @@ func (s *SQLCommon) ResolveOperation(ctx context.Context, id *fftypes.UUID, stat if output != nil { update.Set("output", output) } - return s.updateOperation(ctx, id, update) + return s.UpdateOperation(ctx, id, update) } diff --git a/internal/database/sqlcommon/operation_sql_test.go b/internal/database/sqlcommon/operation_sql_test.go index 27b4fdc1ac..f07d082e8b 100644 --- a/internal/database/sqlcommon/operation_sql_test.go +++ b/internal/database/sqlcommon/operation_sql_test.go @@ -193,7 +193,7 @@ func TestOperationUpdateBeginFail(t *testing.T) { s, mock := newMockProvider().init() mock.ExpectBegin().WillReturnError(fmt.Errorf("pop")) u := database.OperationQueryFactory.NewUpdate(context.Background()).Set("id", fftypes.NewUUID()) - err := s.updateOperation(context.Background(), fftypes.NewUUID(), u) + err := s.UpdateOperation(context.Background(), fftypes.NewUUID(), u) assert.Regexp(t, "FF10114", err) } @@ -201,7 +201,7 @@ func TestOperationUpdateBuildQueryFail(t *testing.T) { s, mock := newMockProvider().init() mock.ExpectBegin() u := database.OperationQueryFactory.NewUpdate(context.Background()).Set("id", map[bool]bool{true: false}) - err := s.updateOperation(context.Background(), fftypes.NewUUID(), u) + err := s.UpdateOperation(context.Background(), fftypes.NewUUID(), u) assert.Regexp(t, "FF10149.*id", err) } @@ -211,6 +211,6 @@ func TestOperationUpdateFail(t *testing.T) { mock.ExpectExec("UPDATE .*").WillReturnError(fmt.Errorf("pop")) mock.ExpectRollback() u := database.OperationQueryFactory.NewUpdate(context.Background()).Set("id", fftypes.NewUUID()) - err := s.updateOperation(context.Background(), fftypes.NewUUID(), u) + err := s.UpdateOperation(context.Background(), fftypes.NewUUID(), u) assert.Regexp(t, "FF10117", err) } diff --git a/internal/operations/manager.go b/internal/operations/manager.go index 4294e88590..a292691516 100644 --- a/internal/operations/manager.go +++ b/internal/operations/manager.go @@ -35,6 +35,7 @@ type Manager interface { RegisterHandler(handler OperationHandler, ops []fftypes.OpType) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) error + RetryOperation(ctx context.Context, ns string, opID *fftypes.UUID) (*fftypes.Operation, error) } type operationsManager struct { @@ -85,6 +86,40 @@ func (om *operationsManager) RunOperation(ctx context.Context, op *fftypes.Prepa return nil } +func (om *operationsManager) RetryOperation(ctx context.Context, ns string, opID *fftypes.UUID) (op *fftypes.Operation, err error) { + var po *fftypes.PreparedOperation + err = om.database.RunAsGroup(ctx, func(ctx context.Context) error { + op, err = om.database.GetOperationByID(ctx, opID) + if err != nil { + return err + } + + // Create a copy of the operation with a new ID + op.ID = fftypes.NewUUID() + op.Status = fftypes.OpStatusPending + op.Output = nil + op.Created = fftypes.Now() + op.Updated = op.Created + if err = om.database.InsertOperation(ctx, op); err != nil { + return err + } + + // Update the old operation to point to the new one + update := database.OperationQueryFactory.NewUpdate(ctx).Set("retry", op.ID) + if err = om.database.UpdateOperation(ctx, opID, update); err != nil { + return err + } + + po, err = om.PrepareOperation(ctx, op) + return err + }) + if err != nil { + return nil, err + } + + return op, om.RunOperation(ctx, po) +} + func (om *operationsManager) writeOperationSuccess(ctx context.Context, opID *fftypes.UUID) { if err := om.database.ResolveOperation(ctx, opID, fftypes.OpStatusSucceeded, "", nil); err != nil { log.L(ctx).Errorf("Failed to update operation %s: %s", opID, err) diff --git a/internal/operations/manager_test.go b/internal/operations/manager_test.go index 26cd181476..d1a3e0c0cc 100644 --- a/internal/operations/manager_test.go +++ b/internal/operations/manager_test.go @@ -22,6 +22,7 @@ import ( "github.com/hyperledger/firefly/internal/config" "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/tokenmocks" + "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/hyperledger/firefly/pkg/tokens" "github.com/stretchr/testify/assert" @@ -31,10 +32,11 @@ import ( type mockHandler struct { Complete bool Err error + Prepared *fftypes.PreparedOperation } func (m *mockHandler) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { - return nil, m.Err + return m.Prepared, m.Err } func (m *mockHandler) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) { @@ -45,6 +47,14 @@ func newTestOperations(t *testing.T) (*operationsManager, func()) { config.Reset() mdi := &databasemocks.Plugin{} mti := &tokenmocks.Plugin{} + + rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() + rag.RunFn = func(a mock.Arguments) { + rag.ReturnArguments = mock.Arguments{ + a[1].(func(context.Context) error)(a[0].(context.Context)), + } + } + mti.On("Name").Return("ut_tokens").Maybe() ctx, cancel := context.WithCancel(context.Background()) om, err := NewOperationsManager(ctx, mdi, map[string]tokens.Plugin{"magic-tokens": mti}) @@ -119,9 +129,11 @@ func TestRunOperationSyncSuccess(t *testing.T) { mdi.On("ResolveOperation", ctx, op.ID, fftypes.OpStatusSucceeded, "", mock.Anything).Return(nil) om.RegisterHandler(&mockHandler{Complete: true}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) - err := om.RunOperation(context.Background(), op) + err := om.RunOperation(ctx, op) assert.NoError(t, err) + + mdi.AssertExpectations(t) } func TestRunOperationFail(t *testing.T) { @@ -138,9 +150,144 @@ func TestRunOperationFail(t *testing.T) { mdi.On("ResolveOperation", ctx, op.ID, fftypes.OpStatusFailed, "pop", mock.Anything).Return(nil) om.RegisterHandler(&mockHandler{Err: fmt.Errorf("pop")}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) - err := om.RunOperation(context.Background(), op) + err := om.RunOperation(ctx, op) + + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestRetryOperationSuccess(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + opID := fftypes.NewUUID() + op := &fftypes.Operation{ + ID: opID, + Plugin: "blockchain", + Type: fftypes.OpTypeBlockchainBatchPin, + Status: fftypes.OpStatusFailed, + } + po := &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("GetOperationByID", ctx, opID).Return(op, nil) + mdi.On("InsertOperation", ctx, mock.MatchedBy(func(newOp *fftypes.Operation) bool { + assert.NotEqual(t, opID, newOp.ID) + assert.Equal(t, "blockchain", newOp.Plugin) + assert.Equal(t, fftypes.OpStatusPending, newOp.Status) + assert.Equal(t, fftypes.OpTypeBlockchainBatchPin, newOp.Type) + return true + })).Return(nil) + mdi.On("UpdateOperation", ctx, op.ID, mock.MatchedBy(func(update database.Update) bool { + info, err := update.Finalize() + assert.NoError(t, err) + assert.Equal(t, 1, len(info.SetOperations)) + assert.Equal(t, "retry", info.SetOperations[0].Field) + val, err := info.SetOperations[0].Value.Value() + assert.NoError(t, err) + assert.Equal(t, op.ID.String(), val) + return true + })).Return(nil) + + om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + newOp, err := om.RetryOperation(ctx, "ns1", op.ID) + + assert.NoError(t, err) + assert.NotNil(t, newOp) + + mdi.AssertExpectations(t) +} + +func TestRetryOperationGetFail(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + opID := fftypes.NewUUID() + op := &fftypes.Operation{ + ID: opID, + Plugin: "blockchain", + Type: fftypes.OpTypeBlockchainBatchPin, + Status: fftypes.OpStatusFailed, + } + po := &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("GetOperationByID", ctx, opID).Return(op, fmt.Errorf("pop")) + + om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + _, err := om.RetryOperation(ctx, "ns1", op.ID) + + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestRetryOperationInsertFail(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + opID := fftypes.NewUUID() + op := &fftypes.Operation{ + ID: opID, + Plugin: "blockchain", + Type: fftypes.OpTypeBlockchainBatchPin, + Status: fftypes.OpStatusFailed, + } + po := &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("GetOperationByID", ctx, opID).Return(op, nil) + mdi.On("InsertOperation", ctx, mock.Anything).Return(fmt.Errorf("pop")) + + om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + _, err := om.RetryOperation(ctx, "ns1", op.ID) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestRetryOperationUpdateFail(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + opID := fftypes.NewUUID() + op := &fftypes.Operation{ + ID: opID, + Plugin: "blockchain", + Type: fftypes.OpTypeBlockchainBatchPin, + Status: fftypes.OpStatusFailed, + } + po := &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("GetOperationByID", ctx, opID).Return(op, nil) + mdi.On("InsertOperation", ctx, mock.Anything).Return(nil) + mdi.On("UpdateOperation", ctx, op.ID, mock.Anything).Return(fmt.Errorf("pop")) + + om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + _, err := om.RetryOperation(ctx, "ns1", op.ID) + + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) } func TestWriteOperationSuccess(t *testing.T) { @@ -156,7 +303,6 @@ func TestWriteOperationSuccess(t *testing.T) { om.writeOperationSuccess(ctx, opID) mdi.AssertExpectations(t) - } func TestWriteOperationFailure(t *testing.T) { @@ -172,5 +318,4 @@ func TestWriteOperationFailure(t *testing.T) { om.writeOperationFailure(ctx, opID, fmt.Errorf("pop")) mdi.AssertExpectations(t) - } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 2fe102ab5a..8d7e1f1135 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -75,6 +75,7 @@ type Orchestrator interface { Contracts() contracts.Manager Metrics() metrics.Manager BatchManager() batch.Manager + Operations() operations.Manager IsPreInit() bool // Status @@ -282,6 +283,10 @@ func (or *orchestrator) Metrics() metrics.Manager { return or.metrics } +func (or *orchestrator) Operations() operations.Manager { + return or.operations +} + func (or *orchestrator) initDatabaseCheckPreinit(ctx context.Context) (err error) { if or.database == nil { diType := config.GetString(config.DatabaseType) diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 2c0dc6e612..fcaca0760e 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -667,6 +667,7 @@ func TestInitOK(t *testing.T) { assert.Equal(t, or.mam, or.Assets()) assert.Equal(t, or.mcm, or.Contracts()) assert.Equal(t, or.mmi, or.Metrics()) + assert.Equal(t, or.mom, or.Operations()) } func TestInitDataExchangeGetNodesFail(t *testing.T) { diff --git a/mocks/databasemocks/plugin.go b/mocks/databasemocks/plugin.go index e910456030..9666d5969c 100644 --- a/mocks/databasemocks/plugin.go +++ b/mocks/databasemocks/plugin.go @@ -2395,6 +2395,20 @@ func (_m *Plugin) UpdateOffset(ctx context.Context, rowID int64, update database return r0 } +// UpdateOperation provides a mock function with given fields: ctx, id, update +func (_m *Plugin) UpdateOperation(ctx context.Context, id *fftypes.UUID, update database.Update) error { + ret := _m.Called(ctx, id, update) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, database.Update) error); ok { + r0 = rf(ctx, id, update) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpdateOrganization provides a mock function with given fields: ctx, id, update func (_m *Plugin) UpdateOrganization(ctx context.Context, id *fftypes.UUID, update database.Update) error { ret := _m.Called(ctx, id, update) diff --git a/mocks/operationmocks/manager.go b/mocks/operationmocks/manager.go index 3339e2bb5d..b8f2677a57 100644 --- a/mocks/operationmocks/manager.go +++ b/mocks/operationmocks/manager.go @@ -44,6 +44,29 @@ func (_m *Manager) RegisterHandler(handler operations.OperationHandler, ops []ff _m.Called(handler, ops) } +// RetryOperation provides a mock function with given fields: ctx, ns, opID +func (_m *Manager) RetryOperation(ctx context.Context, ns string, opID *fftypes.UUID) (*fftypes.Operation, error) { + ret := _m.Called(ctx, ns, opID) + + var r0 *fftypes.Operation + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.UUID) *fftypes.Operation); ok { + r0 = rf(ctx, ns, opID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.Operation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.UUID) error); ok { + r1 = rf(ctx, ns, opID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RunOperation provides a mock function with given fields: ctx, op func (_m *Manager) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) error { ret := _m.Called(ctx, op) diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go index a5a1ca6883..4a744a50f1 100644 --- a/mocks/orchestratormocks/orchestrator.go +++ b/mocks/orchestratormocks/orchestrator.go @@ -26,6 +26,8 @@ import ( networkmap "github.com/hyperledger/firefly/internal/networkmap" + operations "github.com/hyperledger/firefly/internal/operations" + privatemessaging "github.com/hyperledger/firefly/internal/privatemessaging" ) @@ -1238,6 +1240,22 @@ func (_m *Orchestrator) NetworkMap() networkmap.Manager { return r0 } +// Operations provides a mock function with given fields: +func (_m *Orchestrator) Operations() operations.Manager { + ret := _m.Called() + + var r0 operations.Manager + if rf, ok := ret.Get(0).(func() operations.Manager); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(operations.Manager) + } + } + + return r0 +} + // PrivateMessaging provides a mock function with given fields: func (_m *Orchestrator) PrivateMessaging() privatemessaging.Manager { ret := _m.Called() diff --git a/mocks/txcommonmocks/helper.go b/mocks/txcommonmocks/helper.go index bfff72281f..131a9c10af 100644 --- a/mocks/txcommonmocks/helper.go +++ b/mocks/txcommonmocks/helper.go @@ -71,13 +71,3 @@ func (_m *Helper) SubmitNewTransaction(ctx context.Context, ns string, txType ff return r0, r1 } - -// WriteOperationFailure provides a mock function with given fields: ctx, opID, err -func (_m *Helper) WriteOperationFailure(ctx context.Context, opID *fftypes.UUID, err error) { - _m.Called(ctx, opID, err) -} - -// WriteOperationSuccess provides a mock function with given fields: ctx, opID, output -func (_m *Helper) WriteOperationSuccess(ctx context.Context, opID *fftypes.UUID, output fftypes.JSONObject) { - _m.Called(ctx, opID, output) -} diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go index f3fc6b32ef..e25aeb3c0e 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -199,6 +199,9 @@ type iOperationCollection interface { // ResolveOperation - Resolve operation upon completion ResolveOperation(ctx context.Context, id *fftypes.UUID, status fftypes.OpStatus, errorMsg string, output fftypes.JSONObject) (err error) + // UpdateOperation - Update an operation + UpdateOperation(ctx context.Context, id *fftypes.UUID, update Update) (err error) + // GetOperationByID - Get an operation by ID GetOperationByID(ctx context.Context, id *fftypes.UUID) (operation *fftypes.Operation, err error) @@ -748,6 +751,7 @@ var OperationQueryFactory = &queryFields{ "output": &JSONField{}, "created": &TimeField{}, "updated": &TimeField{}, + "retry": &UUIDField{}, } // SubscriptionQueryFactory filter fields for data subscriptions diff --git a/pkg/fftypes/operation.go b/pkg/fftypes/operation.go index 5f6971c5c2..db9c570040 100644 --- a/pkg/fftypes/operation.go +++ b/pkg/fftypes/operation.go @@ -84,6 +84,7 @@ type Operation struct { Output JSONObject `json:"output,omitempty"` Created *FFTime `json:"created,omitempty"` Updated *FFTime `json:"updated,omitempty"` + Retry *UUID `json:"retry,omitempty"` } // PreparedOperation is an operation that has gathered all the raw data ready to send to a plugin From 155dfb45b70518140072202c8f774bc45c9c7f38 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 17 Feb 2022 16:17:50 -0500 Subject: [PATCH 08/10] Make retries transitive Retrying an operation that has already been retried will cause it to look up the newest copy of the operation, and retry that one. In this way, retries will always form a single chain, and attempting to re-run any of them will always add a new one to the end of the chain. Signed-off-by: Andrew Richardson --- internal/operations/manager.go | 14 ++++++++++- internal/operations/manager_test.go | 38 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/internal/operations/manager.go b/internal/operations/manager.go index a292691516..8a780b9fec 100644 --- a/internal/operations/manager.go +++ b/internal/operations/manager.go @@ -86,10 +86,21 @@ func (om *operationsManager) RunOperation(ctx context.Context, op *fftypes.Prepa return nil } +func (om *operationsManager) findLatestRetry(ctx context.Context, opID *fftypes.UUID) (op *fftypes.Operation, err error) { + op, err = om.database.GetOperationByID(ctx, opID) + if err != nil { + return nil, err + } + if op.Retry == nil { + return op, nil + } + return om.findLatestRetry(ctx, op.Retry) +} + func (om *operationsManager) RetryOperation(ctx context.Context, ns string, opID *fftypes.UUID) (op *fftypes.Operation, err error) { var po *fftypes.PreparedOperation err = om.database.RunAsGroup(ctx, func(ctx context.Context) error { - op, err = om.database.GetOperationByID(ctx, opID) + op, err = om.findLatestRetry(ctx, opID) if err != nil { return err } @@ -97,6 +108,7 @@ func (om *operationsManager) RetryOperation(ctx context.Context, ns string, opID // Create a copy of the operation with a new ID op.ID = fftypes.NewUUID() op.Status = fftypes.OpStatusPending + op.Error = "" op.Output = nil op.Created = fftypes.Now() op.Updated = op.Created diff --git a/internal/operations/manager_test.go b/internal/operations/manager_test.go index ee69595d84..0895c0d55a 100644 --- a/internal/operations/manager_test.go +++ b/internal/operations/manager_test.go @@ -231,6 +231,44 @@ func TestRetryOperationGetFail(t *testing.T) { mdi.AssertExpectations(t) } +func TestRetryTwiceOperationInsertFail(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + opID := fftypes.NewUUID() + opID2 := fftypes.NewUUID() + op := &fftypes.Operation{ + ID: opID, + Plugin: "blockchain", + Type: fftypes.OpTypeBlockchainBatchPin, + Status: fftypes.OpStatusFailed, + Retry: opID2, + } + op2 := &fftypes.Operation{ + ID: opID2, + Plugin: "blockchain", + Type: fftypes.OpTypeBlockchainBatchPin, + Status: fftypes.OpStatusFailed, + } + po := &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("GetOperationByID", ctx, opID).Return(op, nil) + mdi.On("GetOperationByID", ctx, opID2).Return(op2, nil) + mdi.On("InsertOperation", ctx, mock.Anything).Return(fmt.Errorf("pop")) + + om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + _, err := om.RetryOperation(ctx, "ns1", op.ID) + + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + func TestRetryOperationInsertFail(t *testing.T) { om, cancel := newTestOperations(t) defer cancel() From 6496d0b822a50984f10b60830e7b3df4bb9ef976 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 22 Feb 2022 16:11:25 -0500 Subject: [PATCH 09/10] Cache duplicate operations when in a retry loop (such as batch dispatch) Signed-off-by: Andrew Richardson --- internal/batch/batch_processor.go | 7 +- internal/batchpin/batchpin.go | 2 +- internal/batchpin/batchpin_test.go | 8 +- internal/broadcast/manager.go | 2 +- internal/broadcast/manager_test.go | 14 +- internal/operations/cache.go | 84 ++++++++++++ internal/operations/cache_test.go | 129 ++++++++++++++++++ internal/operations/manager.go | 1 + internal/privatemessaging/message_test.go | 11 +- internal/privatemessaging/privatemessaging.go | 4 +- .../privatemessaging/privatemessaging_test.go | 21 +-- mocks/operationmocks/manager.go | 14 ++ 12 files changed, 264 insertions(+), 33 deletions(-) create mode 100644 internal/operations/cache.go create mode 100644 internal/operations/cache_test.go diff --git a/internal/batch/batch_processor.go b/internal/batch/batch_processor.go index 4da5c3f1a6..3252e984f0 100644 --- a/internal/batch/batch_processor.go +++ b/internal/batch/batch_processor.go @@ -26,6 +26,7 @@ import ( "time" "github.com/hyperledger/firefly/internal/log" + "github.com/hyperledger/firefly/internal/operations" "github.com/hyperledger/firefly/internal/retry" "github.com/hyperledger/firefly/internal/sysmessaging" "github.com/hyperledger/firefly/internal/txcommon" @@ -476,8 +477,10 @@ func (bp *batchProcessor) persistBatch(batch *fftypes.Batch) (contexts []*fftype func (bp *batchProcessor) dispatchBatch(batch *fftypes.Batch, pins []*fftypes.Bytes32) error { // Call the dispatcher to do the heavy lifting - will only exit if we're closed - return bp.retry.Do(bp.ctx, "batch dispatch", func(attempt int) (retry bool, err error) { - return true, bp.conf.dispatch(bp.ctx, batch, pins) + return operations.RunWithOperationCache(bp.ctx, func(ctx context.Context) error { + return bp.retry.Do(ctx, "batch dispatch", func(attempt int) (retry bool, err error) { + return true, bp.conf.dispatch(ctx, batch, pins) + }) }) } diff --git a/internal/batchpin/batchpin.go b/internal/batchpin/batchpin.go index 9fb2b031a4..350d776b73 100644 --- a/internal/batchpin/batchpin.go +++ b/internal/batchpin/batchpin.go @@ -69,7 +69,7 @@ func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *fftyp batch.Payload.TX.ID, fftypes.OpTypeBlockchainBatchPin) addBatchPinInputs(op, batch.ID, contexts) - if err := bp.database.InsertOperation(ctx, op); err != nil { + if err := bp.operations.AddOrReuseOperation(ctx, op); err != nil { return err } diff --git a/internal/batchpin/batchpin_test.go b/internal/batchpin/batchpin_test.go index a5c88b082d..8185eedc90 100644 --- a/internal/batchpin/batchpin_test.go +++ b/internal/batchpin/batchpin_test.go @@ -78,7 +78,7 @@ func TestSubmitPinnedBatchOk(t *testing.T) { } contexts := []*fftypes.Bytes32{} - mdi.On("InsertOperation", ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + mom.On("AddOrReuseOperation", ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { assert.Equal(t, fftypes.OpTypeBlockchainBatchPin, op.Type) assert.Equal(t, "ut", op.Plugin) assert.Equal(t, *batch.Payload.TX.ID, *op.Transaction) @@ -120,7 +120,7 @@ func TestSubmitPinnedBatchWithMetricsOk(t *testing.T) { } contexts := []*fftypes.Bytes32{} - mdi.On("InsertOperation", ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + mom.On("AddOrReuseOperation", ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { assert.Equal(t, fftypes.OpTypeBlockchainBatchPin, op.Type) assert.Equal(t, "ut", op.Plugin) assert.Equal(t, *batch.Payload.TX.ID, *op.Transaction) @@ -144,7 +144,7 @@ func TestSubmitPinnedBatchOpFail(t *testing.T) { bp := newTestBatchPinSubmitter(t, false) ctx := context.Background() - mdi := bp.database.(*databasemocks.Plugin) + mom := bp.operations.(*operationmocks.Manager) mmi := bp.metrics.(*metricsmocks.Manager) batch := &fftypes.Batch{ @@ -161,7 +161,7 @@ func TestSubmitPinnedBatchOpFail(t *testing.T) { } contexts := []*fftypes.Bytes32{} - mdi.On("InsertOperation", ctx, mock.Anything).Return(fmt.Errorf("pop")) + mom.On("AddOrReuseOperation", ctx, mock.Anything).Return(fmt.Errorf("pop")) mmi.On("IsMetricsEnabled").Return(false) err := bp.SubmitPinnedBatch(ctx, batch, contexts) assert.Regexp(t, "pop", err) diff --git a/internal/broadcast/manager.go b/internal/broadcast/manager.go index b9b4c8792e..f724c4c850 100644 --- a/internal/broadcast/manager.go +++ b/internal/broadcast/manager.go @@ -121,7 +121,7 @@ func (bm *broadcastManager) dispatchBatch(ctx context.Context, batch *fftypes.Ba batch.Payload.TX.ID, fftypes.OpTypePublicStorageBatchBroadcast) addBatchBroadcastInputs(op, batch.ID) - if err := bm.database.InsertOperation(ctx, op); err != nil { + if err := bm.operations.AddOrReuseOperation(ctx, op); err != nil { return err } if err := bm.operations.RunOperation(ctx, opBatchBroadcast(op, batch)); err != nil { diff --git a/internal/broadcast/manager_test.go b/internal/broadcast/manager_test.go index 33e6bb8406..cef25adb7f 100644 --- a/internal/broadcast/manager_test.go +++ b/internal/broadcast/manager_test.go @@ -144,13 +144,13 @@ func TestDispatchBatchInsertOpFail(t *testing.T) { batch := &fftypes.Batch{} - mdi := bm.database.(*databasemocks.Plugin) - mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + mom := bm.operations.(*operationmocks.Manager) + mom.On("AddOrReuseOperation", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) err := bm.dispatchBatch(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) assert.EqualError(t, err, "pop") - mdi.AssertExpectations(t) + mom.AssertExpectations(t) } func TestDispatchBatchUploadFail(t *testing.T) { @@ -159,9 +159,8 @@ func TestDispatchBatchUploadFail(t *testing.T) { batch := &fftypes.Batch{} - mdi := bm.database.(*databasemocks.Plugin) mom := bm.operations.(*operationmocks.Manager) - mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) + mom.On("AddOrReuseOperation", mock.Anything, mock.Anything).Return(nil) mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { data := op.Data.(batchBroadcastData) return op.Type == fftypes.OpTypePublicStorageBatchBroadcast && data.Batch == batch @@ -170,7 +169,6 @@ func TestDispatchBatchUploadFail(t *testing.T) { err := bm.dispatchBatch(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) assert.EqualError(t, err, "pop") - mdi.AssertExpectations(t) mom.AssertExpectations(t) } @@ -185,7 +183,7 @@ func TestDispatchBatchSubmitBatchPinSucceed(t *testing.T) { mdi := bm.database.(*databasemocks.Plugin) mbp := bm.batchpin.(*batchpinmocks.Submitter) mom := bm.operations.(*operationmocks.Manager) - mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) + mom.On("AddOrReuseOperation", mock.Anything, mock.Anything).Return(nil) mbp.On("SubmitPinnedBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { data := op.Data.(batchBroadcastData) @@ -209,7 +207,7 @@ func TestDispatchBatchSubmitBroadcastFail(t *testing.T) { mdi := bm.database.(*databasemocks.Plugin) mbp := bm.batchpin.(*batchpinmocks.Submitter) mom := bm.operations.(*operationmocks.Manager) - mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) + mom.On("AddOrReuseOperation", mock.Anything, mock.Anything).Return(nil) mbp.On("SubmitPinnedBatch", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { data := op.Data.(batchBroadcastData) diff --git a/internal/operations/cache.go b/internal/operations/cache.go new file mode 100644 index 0000000000..79d0482201 --- /dev/null +++ b/internal/operations/cache.go @@ -0,0 +1,84 @@ +// Copyright © 2022 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 operations + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly/internal/log" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +type operationCacheKey struct{} +type operationCache map[string]*fftypes.Operation + +func getOperationCache(ctx context.Context) operationCache { + ctxKey := operationCacheKey{} + cacheVal := ctx.Value(ctxKey) + if cacheVal != nil { + if cache, ok := cacheVal.(operationCache); ok { + return cache + } + } + return nil +} + +func getCacheKey(op *fftypes.Operation) (string, error) { + opCopy := &fftypes.Operation{ + Namespace: op.Namespace, + Transaction: op.Transaction, + Type: op.Type, + Plugin: op.Plugin, + Input: op.Input, + } + key, err := json.Marshal(opCopy) + if err != nil { + return "", err + } + return string(key), nil +} + +func beginCache(ctx context.Context) (ctx1 context.Context) { + l := log.L(ctx).WithField("opcache", fftypes.ShortID()) + ctx1 = log.WithLogger(ctx, l) + return context.WithValue(ctx1, operationCacheKey{}, operationCache{}) +} + +func RunWithOperationCache(ctx context.Context, fn func(ctx context.Context) error) error { + return fn(beginCache(ctx)) +} + +func (om *operationsManager) AddOrReuseOperation(ctx context.Context, op *fftypes.Operation) error { + // If a cache has been created via RunWithOperationCache, detect duplicate operation inserts + cache := getOperationCache(ctx) + if cache != nil { + if cacheKey, err := getCacheKey(op); err == nil { + if cached, ok := cache[cacheKey]; ok { + // Identical operation already added in this context + *op = *cached + return nil + } + if err = om.database.InsertOperation(ctx, op); err != nil { + return err + } + cache[cacheKey] = op + return nil + } + } + return om.database.InsertOperation(ctx, op) +} diff --git a/internal/operations/cache_test.go b/internal/operations/cache_test.go new file mode 100644 index 0000000000..89c55a7dab --- /dev/null +++ b/internal/operations/cache_test.go @@ -0,0 +1,129 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance 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 imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package operations + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestRunWithOperationCache(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + op1 := &fftypes.Operation{ + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{"batch": "1"}, + Status: fftypes.OpStatusFailed, + } + op1Copy := &fftypes.Operation{ + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{"batch": "1"}, + Status: fftypes.OpStatusPending, + } + op2 := &fftypes.Operation{ + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{"batch": "2"}, + Status: fftypes.OpStatusFailed, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("InsertOperation", mock.Anything, op1).Return(nil).Once() + mdi.On("InsertOperation", mock.Anything, op2).Return(nil).Once() + + err := RunWithOperationCache(context.Background(), func(ctx context.Context) error { + if err := om.AddOrReuseOperation(ctx, op1); err != nil { + return err + } + if err := om.AddOrReuseOperation(ctx, op1Copy); err != nil { + return err + } + return om.AddOrReuseOperation(ctx, op2) + }) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestRunWithOperationCacheFail(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + op1 := &fftypes.Operation{ + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{"batch": "1"}, + Status: fftypes.OpStatusFailed, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("InsertOperation", mock.Anything, op1).Return(fmt.Errorf("pop")).Once() + + err := RunWithOperationCache(context.Background(), func(ctx context.Context) error { + return om.AddOrReuseOperation(ctx, op1) + }) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestAddOrReuseOperationNoCache(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + op1 := &fftypes.Operation{ + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{"batch": "1"}, + Status: fftypes.OpStatusFailed, + } + op2 := &fftypes.Operation{ + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Input: fftypes.JSONObject{"batch": "1"}, + Status: fftypes.OpStatusPending, + } + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("InsertOperation", ctx, op1).Return(nil).Once() + mdi.On("InsertOperation", ctx, op2).Return(nil).Once() + + err := om.AddOrReuseOperation(ctx, op1) + assert.NoError(t, err) + err = om.AddOrReuseOperation(ctx, op2) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestGetCacheKeyBadJSON(t *testing.T) { + op := &fftypes.Operation{ + Input: fftypes.JSONObject{ + "test": map[bool]bool{true: false}, + }, + } + _, err := getCacheKey(op) + assert.Error(t, err) +} diff --git a/internal/operations/manager.go b/internal/operations/manager.go index 8a780b9fec..14cc8022fb 100644 --- a/internal/operations/manager.go +++ b/internal/operations/manager.go @@ -36,6 +36,7 @@ type Manager interface { PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) error RetryOperation(ctx context.Context, ns string, opID *fftypes.UUID) (*fftypes.Operation, error) + AddOrReuseOperation(ctx context.Context, op *fftypes.Operation) error } type operationsManager struct { diff --git a/internal/privatemessaging/message_test.go b/internal/privatemessaging/message_test.go index 6abf2b33bf..aa5eb7c8e7 100644 --- a/internal/privatemessaging/message_test.go +++ b/internal/privatemessaging/message_test.go @@ -697,7 +697,7 @@ func TestDispatchedUnpinnedMessageOK(t *testing.T) { mdi.On("GetNodeByID", pm.ctx, nodeID2).Return(&fftypes.Node{ ID: nodeID2, Name: "node2", Owner: "org1", DX: fftypes.DXInfo{Peer: "peer2-remote"}, }, nil).Once() - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) + mom.On("AddOrReuseOperation", pm.ctx, mock.Anything).Return(nil) mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { data := op.Data.(batchSendData) return op.Type == fftypes.OpTypeDataExchangeBatchSend && *data.Node.ID == *nodeID2 @@ -794,10 +794,8 @@ func TestSendDataTransferFail(t *testing.T) { mim := pm.identity.(*identitymanagermocks.Manager) mim.On("GetLocalOrgKey", pm.ctx).Return("localorg", nil) - mdi := pm.database.(*databasemocks.Plugin) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) - mom := pm.operations.(*operationmocks.Manager) + mom.On("AddOrReuseOperation", pm.ctx, mock.Anything).Return(nil) mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { data := op.Data.(batchSendData) return op.Type == fftypes.OpTypeDataExchangeBatchSend && *data.Node.ID == *nodeID2 @@ -829,7 +827,6 @@ func TestSendDataTransferFail(t *testing.T) { assert.Regexp(t, "pop", err) mim.AssertExpectations(t) - mdi.AssertExpectations(t) mom.AssertExpectations(t) } @@ -846,8 +843,8 @@ func TestSendDataTransferInsertOperationFail(t *testing.T) { })).Return(nil) mim.On("GetLocalOrgKey", pm.ctx).Return("localorg", nil) - mdi := pm.database.(*databasemocks.Plugin) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(fmt.Errorf("pop")) + mom := pm.operations.(*operationmocks.Manager) + mom.On("AddOrReuseOperation", pm.ctx, mock.Anything).Return(fmt.Errorf("pop")) groupID := fftypes.NewRandB32() nodeID2 := fftypes.NewUUID() diff --git a/internal/privatemessaging/privatemessaging.go b/internal/privatemessaging/privatemessaging.go index 904f39507d..5d964193e2 100644 --- a/internal/privatemessaging/privatemessaging.go +++ b/internal/privatemessaging/privatemessaging.go @@ -198,7 +198,7 @@ func (pm *privateMessaging) transferBlobs(ctx context.Context, data []*fftypes.D txid, fftypes.OpTypeDataExchangeBlobSend) addTransferBlobInputs(op, node.ID, blob.Hash) - if err = pm.database.InsertOperation(ctx, op); err != nil { + if err = pm.operations.AddOrReuseOperation(ctx, op); err != nil { return err } @@ -243,7 +243,7 @@ func (pm *privateMessaging) sendData(ctx context.Context, tw *fftypes.TransportW groupHash = tw.Group.Hash } addBatchSendInputs(op, node.ID, groupHash, batch.ID, tw.Batch.Manifest().String()) - if err = pm.database.InsertOperation(ctx, op); err != nil { + if err = pm.operations.AddOrReuseOperation(ctx, op); err != nil { return err } if err = pm.operations.RunOperation(ctx, opBatchSend(op, node, tw)); err != nil { diff --git a/internal/privatemessaging/privatemessaging_test.go b/internal/privatemessaging/privatemessaging_test.go index e107f6be23..d0e14b4f76 100644 --- a/internal/privatemessaging/privatemessaging_test.go +++ b/internal/privatemessaging/privatemessaging_test.go @@ -152,16 +152,16 @@ func TestDispatchBatchWithBlobs(t *testing.T) { Hash: blob1, PayloadRef: "/blob/1", }, nil) - mdi.On("InsertOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + mom.On("AddOrReuseOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBlobSend })).Return(nil, nil) - mdi.On("InsertOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + mom.On("AddOrReuseOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBlobSend })).Return(nil, nil) - mdi.On("InsertOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + mom.On("AddOrReuseOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBatchSend })).Return(nil, nil) - mdi.On("InsertOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + mom.On("AddOrReuseOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBatchSend })).Return(nil, nil) mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { @@ -313,7 +313,9 @@ func TestSendSubmitInsertOperationFail(t *testing.T) { mdi := pm.database.(*databasemocks.Plugin) mdi.On("GetGroupByHash", pm.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(fmt.Errorf("pop")) + + mom := pm.operations.(*operationmocks.Manager) + mom.On("AddOrReuseOperation", pm.ctx, mock.Anything).Return(fmt.Errorf("pop")) err := pm.dispatchPinnedBatch(pm.ctx, &fftypes.Batch{ Identity: fftypes.Identity{ @@ -359,7 +361,9 @@ func TestWriteTransactionSubmitBatchPinFail(t *testing.T) { mdi := pm.database.(*databasemocks.Plugin) mdi.On("GetGroupByHash", pm.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")) mdi.On("UpsertTransaction", pm.ctx, mock.Anything, true, false).Return(nil) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) + + mom := pm.operations.(*operationmocks.Manager) + mom.On("AddOrReuseOperation", pm.ctx, mock.Anything).Return(nil) mbp := pm.batchpin.(*batchpinmocks.Submitter) mbp.On("SubmitPinnedBatch", pm.ctx, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) @@ -394,7 +398,7 @@ func TestTransferBlobsFail(t *testing.T) { node := &fftypes.Node{ID: fftypes.NewUUID(), DX: fftypes.DXInfo{Peer: "peer1"}} mdi.On("GetBlobMatchingHash", pm.ctx, mock.Anything).Return(&fftypes.Blob{PayloadRef: "blob/1"}, nil) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) + mom.On("AddOrReuseOperation", pm.ctx, mock.Anything).Return(nil) mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { if op.Type != fftypes.OpTypeDataExchangeBlobSend { return false @@ -418,10 +422,11 @@ func TestTransferBlobsOpInsertFail(t *testing.T) { mdi := pm.database.(*databasemocks.Plugin) mdx := pm.exchange.(*dataexchangemocks.Plugin) + mom := pm.operations.(*operationmocks.Manager) mdi.On("GetBlobMatchingHash", pm.ctx, mock.Anything).Return(&fftypes.Blob{PayloadRef: "blob/1"}, nil) mdx.On("TransferBLOB", pm.ctx, mock.Anything, "peer1", "blob/1").Return(nil) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(fmt.Errorf("pop")) + mom.On("AddOrReuseOperation", pm.ctx, mock.Anything).Return(fmt.Errorf("pop")) err := pm.transferBlobs(pm.ctx, []*fftypes.Data{ {ID: fftypes.NewUUID(), Hash: fftypes.NewRandB32(), Blob: &fftypes.BlobRef{Hash: fftypes.NewRandB32()}}, diff --git a/mocks/operationmocks/manager.go b/mocks/operationmocks/manager.go index b8f2677a57..9ca7aed36f 100644 --- a/mocks/operationmocks/manager.go +++ b/mocks/operationmocks/manager.go @@ -16,6 +16,20 @@ type Manager struct { mock.Mock } +// AddOrReuseOperation provides a mock function with given fields: ctx, op +func (_m *Manager) AddOrReuseOperation(ctx context.Context, op *fftypes.Operation) error { + ret := _m.Called(ctx, op) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) error); ok { + r0 = rf(ctx, op) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // PrepareOperation provides a mock function with given fields: ctx, op func (_m *Manager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { ret := _m.Called(ctx, op) From b836bcdd9bb873999f359162cbc8671d8e50cd3c Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 1 Mar 2022 16:40:05 -0500 Subject: [PATCH 10/10] Add names to all operation handlers Signed-off-by: Andrew Richardson --- internal/assets/manager.go | 8 ++++++- internal/assets/manager_test.go | 10 ++++++-- internal/batchpin/batchpin.go | 8 ++++++- internal/batchpin/batchpin_test.go | 7 +++++- internal/broadcast/manager.go | 8 ++++++- internal/broadcast/manager_test.go | 8 ++++++- internal/contracts/manager.go | 8 ++++++- internal/contracts/manager_test.go | 9 +++++-- internal/operations/manager.go | 8 +++++-- internal/operations/manager_test.go | 24 ++++++++++++------- internal/privatemessaging/privatemessaging.go | 7 +++++- .../privatemessaging/privatemessaging_test.go | 8 ++++++- mocks/assetmocks/manager.go | 14 +++++++++++ mocks/batchpinmocks/submitter.go | 14 +++++++++++ mocks/broadcastmocks/manager.go | 14 +++++++++++ mocks/contractmocks/manager.go | 14 +++++++++++ mocks/operationmocks/manager.go | 6 ++--- mocks/privatemessagingmocks/manager.go | 14 +++++++++++ 18 files changed, 163 insertions(+), 26 deletions(-) diff --git a/internal/assets/manager.go b/internal/assets/manager.go index 30064722e6..db4a2a998f 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -35,6 +35,8 @@ import ( ) type Manager interface { + fftypes.Named + CreateTokenPool(ctx context.Context, ns string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, blockchainInfo fftypes.JSONObject) error GetTokenPools(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) @@ -95,7 +97,7 @@ func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manage metrics: mm, operations: om, } - om.RegisterHandler(am, []fftypes.OpType{ + om.RegisterHandler(ctx, am, []fftypes.OpType{ fftypes.OpTypeTokenCreatePool, fftypes.OpTypeTokenActivatePool, fftypes.OpTypeTokenTransfer, @@ -104,6 +106,10 @@ func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manage return am, nil } +func (am *assetManager) Name() string { + return "AssetManager" +} + func (am *assetManager) selectTokenPlugin(ctx context.Context, name string) (tokens.Plugin, error) { for pluginName, plugin := range am.tokens { if pluginName == name { diff --git a/internal/assets/manager_test.go b/internal/assets/manager_test.go index eca5d1a248..b32da3d7e4 100644 --- a/internal/assets/manager_test.go +++ b/internal/assets/manager_test.go @@ -49,7 +49,7 @@ func newTestAssets(t *testing.T) (*assetManager, func()) { mom := &operationmocks.Manager{} mti.On("Name").Return("ut_tokens").Maybe() mm.On("IsMetricsEnabled").Return(false) - mom.On("RegisterHandler", mock.Anything, mock.Anything) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) ctx, cancel := context.WithCancel(context.Background()) a, err := NewAssetManager(ctx, mdi, mim, mdm, msa, mbm, mpm, map[string]tokens.Plugin{"magic-tokens": mti}, mm, mom) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() @@ -76,7 +76,7 @@ func newTestAssetsWithMetrics(t *testing.T) (*assetManager, func()) { mti.On("Name").Return("ut_tokens").Maybe() mm.On("IsMetricsEnabled").Return(true) mm.On("TransferSubmitted", mock.Anything) - mom.On("RegisterHandler", mock.Anything, mock.Anything) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) ctx, cancel := context.WithCancel(context.Background()) a, err := NewAssetManager(ctx, mdi, mim, mdm, msa, mbm, mpm, map[string]tokens.Plugin{"magic-tokens": mti}, mm, mom) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() @@ -94,6 +94,12 @@ func TestInitFail(t *testing.T) { assert.Regexp(t, "FF10128", err) } +func TestName(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + assert.Equal(t, "AssetManager", am.Name()) +} + func TestGetTokenBalances(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() diff --git a/internal/batchpin/batchpin.go b/internal/batchpin/batchpin.go index 350d776b73..d480f790e4 100644 --- a/internal/batchpin/batchpin.go +++ b/internal/batchpin/batchpin.go @@ -29,6 +29,8 @@ import ( ) type Submitter interface { + fftypes.Named + SubmitPinnedBatch(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error // From operations.OperationHandler @@ -55,12 +57,16 @@ func NewBatchPinSubmitter(ctx context.Context, di database.Plugin, im identity.M metrics: mm, operations: om, } - om.RegisterHandler(bp, []fftypes.OpType{ + om.RegisterHandler(ctx, bp, []fftypes.OpType{ fftypes.OpTypeBlockchainBatchPin, }) return bp, nil } +func (bp *batchPinSubmitter) Name() string { + return "BatchPinSubmitter" +} + func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error { // The pending blockchain transaction op := fftypes.NewOperation( diff --git a/internal/batchpin/batchpin_test.go b/internal/batchpin/batchpin_test.go index 8185eedc90..733f0c044f 100644 --- a/internal/batchpin/batchpin_test.go +++ b/internal/batchpin/batchpin_test.go @@ -41,7 +41,7 @@ func newTestBatchPinSubmitter(t *testing.T, enableMetrics bool) *batchPinSubmitt mmi := &metricsmocks.Manager{} mom := &operationmocks.Manager{} mmi.On("IsMetricsEnabled").Return(enableMetrics) - mom.On("RegisterHandler", mock.Anything, mock.Anything) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) if enableMetrics { mmi.On("CountBatchPin").Return() } @@ -56,6 +56,11 @@ func TestInitFail(t *testing.T) { assert.Regexp(t, "FF10128", err) } +func TestName(t *testing.T) { + bp := newTestBatchPinSubmitter(t, false) + assert.Equal(t, "BatchPinSubmitter", bp.Name()) +} + func TestSubmitPinnedBatchOk(t *testing.T) { bp := newTestBatchPinSubmitter(t, false) ctx := context.Background() diff --git a/internal/broadcast/manager.go b/internal/broadcast/manager.go index f724c4c850..fbd7027969 100644 --- a/internal/broadcast/manager.go +++ b/internal/broadcast/manager.go @@ -40,6 +40,8 @@ import ( const broadcastDispatcherName = "pinned_broadcast" type Manager interface { + fftypes.Named + NewBroadcast(ns string, in *fftypes.MessageInOut) sysmessaging.MessageSender BroadcastDatatype(ctx context.Context, ns string, datatype *fftypes.Datatype, waitConfirm bool) (msg *fftypes.Message, err error) BroadcastNamespace(ctx context.Context, ns *fftypes.Namespace, waitConfirm bool) (msg *fftypes.Message, err error) @@ -107,13 +109,17 @@ func NewBroadcastManager(ctx context.Context, di database.Plugin, im identity.Ma fftypes.MessageTypeTransferBroadcast, }, bm.dispatchBatch, bo) - om.RegisterHandler(bm, []fftypes.OpType{ + om.RegisterHandler(ctx, bm, []fftypes.OpType{ fftypes.OpTypePublicStorageBatchBroadcast, }) return bm, nil } +func (bm *broadcastManager) Name() string { + return "BroadcastManager" +} + func (bm *broadcastManager) dispatchBatch(ctx context.Context, batch *fftypes.Batch, pins []*fftypes.Bytes32) error { op := fftypes.NewOperation( bm.publicstorage, diff --git a/internal/broadcast/manager_test.go b/internal/broadcast/manager_test.go index cef25adb7f..2943a9adf6 100644 --- a/internal/broadcast/manager_test.go +++ b/internal/broadcast/manager_test.go @@ -66,7 +66,7 @@ func newTestBroadcastCommon(t *testing.T, metricsEnabled bool) (*broadcastManage fftypes.MessageTypeDefinition, fftypes.MessageTypeTransferBroadcast, }, mock.Anything, mock.Anything).Return() - mom.On("RegisterHandler", mock.Anything, mock.Anything) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { @@ -97,6 +97,12 @@ func TestInitFail(t *testing.T) { assert.Regexp(t, "FF10128", err) } +func TestName(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + assert.Equal(t, "BroadcastManager", bm.Name()) +} + func TestBroadcastMessageGood(t *testing.T) { bm, cancel := newTestBroadcast(t) defer cancel() diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index 5101fa54ee..3c6209b1d5 100644 --- a/internal/contracts/manager.go +++ b/internal/contracts/manager.go @@ -34,6 +34,8 @@ import ( ) type Manager interface { + fftypes.Named + BroadcastFFI(ctx context.Context, ns string, ffi *fftypes.FFI, waitConfirm bool) (output *fftypes.FFI, err error) GetFFI(ctx context.Context, ns, name, version string) (*fftypes.FFI, error) GetFFIByID(ctx context.Context, id *fftypes.UUID) (*fftypes.FFI, error) @@ -93,13 +95,17 @@ func NewContractManager(ctx context.Context, di database.Plugin, ps publicstorag operations: om, } - om.RegisterHandler(cm, []fftypes.OpType{ + om.RegisterHandler(ctx, cm, []fftypes.OpType{ fftypes.OpTypeBlockchainInvoke, }) return cm, nil } +func (cm *contractManager) Name() string { + return "ContractManager" +} + func (cm *contractManager) newFFISchemaCompiler() *jsonschema.Compiler { c := fftypes.NewFFISchemaCompiler() if cm.ffiParamValidator != nil { diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index 0049cffdcc..d24fbe8137 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -44,7 +44,7 @@ func newTestContractManager() *contractManager { mbi := &blockchainmocks.Plugin{} mom := &operationmocks.Manager{} mbi.On("GetFFIParamValidator", mock.Anything).Return(nil, nil) - mom.On("RegisterHandler", mock.Anything, mock.Anything) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) mbi.On("Name").Return("mockblockchain").Maybe() @@ -64,6 +64,11 @@ func TestNewContractManagerFail(t *testing.T) { assert.Regexp(t, "FF10128", err) } +func TestName(t *testing.T) { + cm := newTestContractManager() + assert.Equal(t, "ContractManager", cm.Name()) +} + func TestNewContractManagerFFISchemaLoaderFail(t *testing.T) { mdb := &databasemocks.Plugin{} mps := &publicstoragemocks.Plugin{} @@ -84,7 +89,7 @@ func TestNewContractManagerFFISchemaLoader(t *testing.T) { mbi := &blockchainmocks.Plugin{} mom := &operationmocks.Manager{} mbi.On("GetFFIParamValidator", mock.Anything).Return(ðereum.FFIParamValidator{}, nil) - mom.On("RegisterHandler", mock.Anything, mock.Anything) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) _, err := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi, mom) assert.NoError(t, err) } diff --git a/internal/operations/manager.go b/internal/operations/manager.go index 14cc8022fb..6b774543a9 100644 --- a/internal/operations/manager.go +++ b/internal/operations/manager.go @@ -27,12 +27,13 @@ import ( ) type OperationHandler interface { + fftypes.Named PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) (complete bool, err error) } type Manager interface { - RegisterHandler(handler OperationHandler, ops []fftypes.OpType) + RegisterHandler(ctx context.Context, handler OperationHandler, ops []fftypes.OpType) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) RunOperation(ctx context.Context, op *fftypes.PreparedOperation) error RetryOperation(ctx context.Context, ns string, opID *fftypes.UUID) (*fftypes.Operation, error) @@ -59,8 +60,9 @@ func NewOperationsManager(ctx context.Context, di database.Plugin, ti map[string return om, nil } -func (om *operationsManager) RegisterHandler(handler OperationHandler, ops []fftypes.OpType) { +func (om *operationsManager) RegisterHandler(ctx context.Context, handler OperationHandler, ops []fftypes.OpType) { for _, opType := range ops { + log.L(ctx).Debugf("OpType=%s registered to handler %s", opType, handler.Name()) om.handlers[opType] = handler } } @@ -78,6 +80,8 @@ func (om *operationsManager) RunOperation(ctx context.Context, op *fftypes.Prepa if !ok { return i18n.NewError(ctx, i18n.MsgOperationNotSupported) } + log.L(ctx).Infof("Executing %s operation %s via handler %s", op.Type, op.ID, handler.Name()) + log.L(ctx).Tracef("Operation detail: %+v", op) if complete, err := handler.RunOperation(ctx, op); err != nil { om.writeOperationFailure(ctx, op.ID, err) return err diff --git a/internal/operations/manager_test.go b/internal/operations/manager_test.go index 0895c0d55a..112c21f43b 100644 --- a/internal/operations/manager_test.go +++ b/internal/operations/manager_test.go @@ -35,6 +35,10 @@ type mockHandler struct { Prepared *fftypes.PreparedOperation } +func (m *mockHandler) Name() string { + return "MockHandler" +} + func (m *mockHandler) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { return m.Prepared, m.Err } @@ -81,11 +85,12 @@ func TestPrepareOperationSuccess(t *testing.T) { om, cancel := newTestOperations(t) defer cancel() + ctx := context.Background() op := &fftypes.Operation{ Type: fftypes.OpTypeBlockchainBatchPin, } - om.RegisterHandler(&mockHandler{}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) _, err := om.PrepareOperation(context.Background(), op) assert.NoError(t, err) @@ -105,11 +110,12 @@ func TestRunOperationSuccess(t *testing.T) { om, cancel := newTestOperations(t) defer cancel() + ctx := context.Background() op := &fftypes.PreparedOperation{ Type: fftypes.OpTypeBlockchainBatchPin, } - om.RegisterHandler(&mockHandler{}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) err := om.RunOperation(context.Background(), op) assert.NoError(t, err) @@ -128,7 +134,7 @@ func TestRunOperationSyncSuccess(t *testing.T) { mdi := om.database.(*databasemocks.Plugin) mdi.On("ResolveOperation", ctx, op.ID, fftypes.OpStatusSucceeded, "", mock.Anything).Return(nil) - om.RegisterHandler(&mockHandler{Complete: true}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{Complete: true}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) err := om.RunOperation(ctx, op) assert.NoError(t, err) @@ -149,7 +155,7 @@ func TestRunOperationFail(t *testing.T) { mdi := om.database.(*databasemocks.Plugin) mdi.On("ResolveOperation", ctx, op.ID, fftypes.OpStatusFailed, "pop", mock.Anything).Return(nil) - om.RegisterHandler(&mockHandler{Err: fmt.Errorf("pop")}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{Err: fmt.Errorf("pop")}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) err := om.RunOperation(ctx, op) assert.EqualError(t, err, "pop") @@ -194,7 +200,7 @@ func TestRetryOperationSuccess(t *testing.T) { return true })).Return(nil) - om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) newOp, err := om.RetryOperation(ctx, "ns1", op.ID) assert.NoError(t, err) @@ -223,7 +229,7 @@ func TestRetryOperationGetFail(t *testing.T) { mdi := om.database.(*databasemocks.Plugin) mdi.On("GetOperationByID", ctx, opID).Return(op, fmt.Errorf("pop")) - om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) _, err := om.RetryOperation(ctx, "ns1", op.ID) assert.EqualError(t, err, "pop") @@ -261,7 +267,7 @@ func TestRetryTwiceOperationInsertFail(t *testing.T) { mdi.On("GetOperationByID", ctx, opID2).Return(op2, nil) mdi.On("InsertOperation", ctx, mock.Anything).Return(fmt.Errorf("pop")) - om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) _, err := om.RetryOperation(ctx, "ns1", op.ID) assert.EqualError(t, err, "pop") @@ -290,7 +296,7 @@ func TestRetryOperationInsertFail(t *testing.T) { mdi.On("GetOperationByID", ctx, opID).Return(op, nil) mdi.On("InsertOperation", ctx, mock.Anything).Return(fmt.Errorf("pop")) - om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) _, err := om.RetryOperation(ctx, "ns1", op.ID) assert.EqualError(t, err, "pop") @@ -320,7 +326,7 @@ func TestRetryOperationUpdateFail(t *testing.T) { mdi.On("InsertOperation", ctx, mock.Anything).Return(nil) mdi.On("UpdateOperation", ctx, op.ID, mock.Anything).Return(fmt.Errorf("pop")) - om.RegisterHandler(&mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + om.RegisterHandler(ctx, &mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) _, err := om.RetryOperation(ctx, "ns1", op.ID) assert.EqualError(t, err, "pop") diff --git a/internal/privatemessaging/privatemessaging.go b/internal/privatemessaging/privatemessaging.go index 5d964193e2..a278944fc2 100644 --- a/internal/privatemessaging/privatemessaging.go +++ b/internal/privatemessaging/privatemessaging.go @@ -42,6 +42,7 @@ const pinnedPrivateDispatcherName = "pinned_private" const unpinnedPrivateDispatcherName = "unpinned_private" type Manager interface { + fftypes.Named GroupManager Start() error @@ -135,7 +136,7 @@ func NewPrivateMessaging(ctx context.Context, di database.Plugin, im identity.Ma }, pm.dispatchUnpinnedBatch, bo) - om.RegisterHandler(pm, []fftypes.OpType{ + om.RegisterHandler(ctx, pm, []fftypes.OpType{ fftypes.OpTypeDataExchangeBlobSend, fftypes.OpTypeDataExchangeBatchSend, }) @@ -143,6 +144,10 @@ func NewPrivateMessaging(ctx context.Context, di database.Plugin, im identity.Ma return pm, nil } +func (pm *privateMessaging) Name() string { + return "PrivateMessaging" +} + func (pm *privateMessaging) Start() error { return pm.exchange.Start() } diff --git a/internal/privatemessaging/privatemessaging_test.go b/internal/privatemessaging/privatemessaging_test.go index d0e14b4f76..3baf5cba6d 100644 --- a/internal/privatemessaging/privatemessaging_test.go +++ b/internal/privatemessaging/privatemessaging_test.go @@ -70,7 +70,7 @@ func newTestPrivateMessagingCommon(t *testing.T, metricsEnabled bool) (*privateM fftypes.MessageTypePrivate, }, mock.Anything, mock.Anything).Return() mmi.On("IsMetricsEnabled").Return(metricsEnabled) - mom.On("RegisterHandler", mock.Anything, mock.Anything) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { @@ -101,6 +101,12 @@ func newTestPrivateMessagingWithMetrics(t *testing.T) (*privateMessaging, func() return pm, cancel } +func TestName(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + assert.Equal(t, "PrivateMessaging", pm.Name()) +} + func TestDispatchBatchWithBlobs(t *testing.T) { pm, cancel := newTestPrivateMessaging(t) diff --git a/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index 443b420ca8..b2618dd2df 100644 --- a/mocks/assetmocks/manager.go +++ b/mocks/assetmocks/manager.go @@ -385,6 +385,20 @@ func (_m *Manager) MintTokens(ctx context.Context, ns string, transfer *fftypes. return r0, r1 } +// Name provides a mock function with given fields: +func (_m *Manager) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // NewApproval provides a mock function with given fields: ns, approve func (_m *Manager) NewApproval(ns string, approve *fftypes.TokenApprovalInput) sysmessaging.MessageSender { ret := _m.Called(ns, approve) diff --git a/mocks/batchpinmocks/submitter.go b/mocks/batchpinmocks/submitter.go index 810d816e85..612c3ef257 100644 --- a/mocks/batchpinmocks/submitter.go +++ b/mocks/batchpinmocks/submitter.go @@ -14,6 +14,20 @@ type Submitter struct { mock.Mock } +// Name provides a mock function with given fields: +func (_m *Submitter) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // PrepareOperation provides a mock function with given fields: ctx, op func (_m *Submitter) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { ret := _m.Called(ctx, op) diff --git a/mocks/broadcastmocks/manager.go b/mocks/broadcastmocks/manager.go index 555e3b1aa9..c52b98ed1e 100644 --- a/mocks/broadcastmocks/manager.go +++ b/mocks/broadcastmocks/manager.go @@ -177,6 +177,20 @@ func (_m *Manager) BroadcastTokenPool(ctx context.Context, ns string, pool *ffty return r0, r1 } +// Name provides a mock function with given fields: +func (_m *Manager) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // NewBroadcast provides a mock function with given fields: ns, in func (_m *Manager) NewBroadcast(ns string, in *fftypes.MessageInOut) sysmessaging.MessageSender { ret := _m.Called(ns, in) diff --git a/mocks/contractmocks/manager.go b/mocks/contractmocks/manager.go index ebb468dc1d..bc89ea5a3c 100644 --- a/mocks/contractmocks/manager.go +++ b/mocks/contractmocks/manager.go @@ -380,6 +380,20 @@ func (_m *Manager) InvokeContractAPI(ctx context.Context, ns string, apiName str return r0, r1 } +// Name provides a mock function with given fields: +func (_m *Manager) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // PrepareOperation provides a mock function with given fields: ctx, op func (_m *Manager) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { ret := _m.Called(ctx, op) diff --git a/mocks/operationmocks/manager.go b/mocks/operationmocks/manager.go index 9ca7aed36f..21de2f5bfe 100644 --- a/mocks/operationmocks/manager.go +++ b/mocks/operationmocks/manager.go @@ -53,9 +53,9 @@ func (_m *Manager) PrepareOperation(ctx context.Context, op *fftypes.Operation) return r0, r1 } -// RegisterHandler provides a mock function with given fields: handler, ops -func (_m *Manager) RegisterHandler(handler operations.OperationHandler, ops []fftypes.FFEnum) { - _m.Called(handler, ops) +// RegisterHandler provides a mock function with given fields: ctx, handler, ops +func (_m *Manager) RegisterHandler(ctx context.Context, handler operations.OperationHandler, ops []fftypes.FFEnum) { + _m.Called(ctx, handler, ops) } // RetryOperation provides a mock function with given fields: ctx, ns, opID diff --git a/mocks/privatemessagingmocks/manager.go b/mocks/privatemessagingmocks/manager.go index afa2c147f8..68617e0ad7 100644 --- a/mocks/privatemessagingmocks/manager.go +++ b/mocks/privatemessagingmocks/manager.go @@ -94,6 +94,20 @@ func (_m *Manager) GetGroupsNS(ctx context.Context, ns string, filter database.A return r0, r1, r2 } +// Name provides a mock function with given fields: +func (_m *Manager) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // NewMessage provides a mock function with given fields: ns, msg func (_m *Manager) NewMessage(ns string, msg *fftypes.MessageInOut) sysmessaging.MessageSender { ret := _m.Called(ns, msg)