diff --git a/Makefile b/Makefile index a95f768897..61c5777097 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/db/migrations/postgres/000069_add_operation_retry.down.sql b/db/migrations/postgres/000069_add_operation_retry.down.sql new file mode 100644 index 0000000000..e315703994 --- /dev/null +++ b/db/migrations/postgres/000069_add_operation_retry.down.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE operations DROP COLUMN retry_id; +COMMIT; diff --git a/db/migrations/postgres/000069_add_operation_retry.up.sql b/db/migrations/postgres/000069_add_operation_retry.up.sql new file mode 100644 index 0000000000..8c3db8d2a3 --- /dev/null +++ b/db/migrations/postgres/000069_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/000069_add_operation_retry.down.sql b/db/migrations/sqlite/000069_add_operation_retry.down.sql new file mode 100644 index 0000000000..0415eac239 --- /dev/null +++ b/db/migrations/sqlite/000069_add_operation_retry.down.sql @@ -0,0 +1 @@ +ALTER TABLE operations DROP COLUMN retry_id; diff --git a/db/migrations/sqlite/000069_add_operation_retry.up.sql b/db/migrations/sqlite/000069_add_operation_retry.up.sql new file mode 100644 index 0000000000..16668cf665 --- /dev/null +++ b/db/migrations/sqlite/000069_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 9f112ada0c..f82bd14ccf 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -5092,6 +5092,7 @@ paths: type: object plugin: type: string + retry: {} status: type: string tx: {} @@ -5781,6 +5782,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 @@ -5855,6 +5861,7 @@ paths: type: object plugin: type: string + retry: {} status: type: string tx: {} @@ -5920,6 +5927,78 @@ paths: type: object plugin: type: string + retry: {} + status: + type: string + tx: {} + type: + enum: + - blockchain_batch_pin + - blockchain_invoke + - sharedstorage_batch_broadcast + - dataexchange_batch_send + - dataexchange_blob_send + - token_create_pool + - token_activate_pool + - token_transfer + - token_approval + 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: {} @@ -8795,6 +8874,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 1080d838e6..70026210c5 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -69,6 +69,7 @@ var routes = []*oapispec.Route{ getNamespaces, getOpByID, getOps, + postOpRetry, getStatus, getStatusBatchManager, getSubscriptionByID, diff --git a/internal/assets/manager.go b/internal/assets/manager.go index e9be352892..e1f3633087 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -25,8 +25,8 @@ import ( "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" @@ -36,8 +36,10 @@ 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, 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) @@ -60,8 +62,9 @@ type Manager interface { TokenApproval(ctx context.Context, ns string, approval *fftypes.TokenApprovalInput, waitConfirm bool) (*fftypes.TokenApproval, error) GetTokenApprovals(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenApproval, *database.FilterResult, 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 assetManager struct { @@ -74,36 +77,42 @@ type assetManager struct { broadcast broadcast.Manager messaging privatemessaging.Manager tokens map[string]tokens.Plugin - retry retry.Retry metrics metrics.Manager + operations operations.Manager keyNormalization int } -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, 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{ - 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), - }, + ctx: ctx, + database: di, + txHelper: txcommon.NewTransactionHelper(di), + identity: im, + data: dm, + syncasync: sa, + broadcast: bm, + messaging: pm, + tokens: ti, keyNormalization: identity.ParseKeyNormalizationConfig(config.GetString(config.AssetManagerKeyNormalization)), metrics: mm, + operations: om, } + om.RegisterHandler(ctx, am, []fftypes.OpType{ + fftypes.OpTypeTokenCreatePool, + fftypes.OpTypeTokenActivatePool, + fftypes.OpTypeTokenTransfer, + fftypes.OpTypeTokenApproval, + }) 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 { @@ -147,14 +156,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..b32da3d7e4 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,12 @@ 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) + 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) + 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 +72,13 @@ 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) + 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) + 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,16 +90,14 @@ 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) { +func TestName(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() - - am.Start() - am.WaitStop() + assert.Equal(t, "AssetManager", am.Name()) } func TestGetTokenBalances(t *testing.T) { diff --git a/internal/assets/operations.go b/internal/assets/operations.go new file mode 100644 index 0000000000..7018f3fab9 --- /dev/null +++ b/internal/assets/operations.go @@ -0,0 +1,174 @@ +// 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 `json:"pool"` +} + +type activatePoolData struct { + Pool *fftypes.TokenPool `json:"pool"` + BlockchainInfo fftypes.JSONObject `json:"blockchainInfo"` +} + +type transferData struct { + Pool *fftypes.TokenPool `json:"pool"` + Transfer *fftypes.TokenTransfer `json:"transfer"` +} + +type approvalData struct { + Pool *fftypes.TokenPool `json:"pool"` + Approval *fftypes.TokenApproval `json:"approval"` +} + +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 + + case fftypes.OpTypeTokenApproval: + approval, err := txcommon.RetrieveTokenApprovalInputs(ctx, op) + if err != nil { + return nil, err + } + pool, err := am.database.GetTokenPoolByID(ctx, approval.Pool) + if err != nil { + return nil, err + } else if pool == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + } + return opApproval(op, pool, approval), 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.Pool.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)) + } + + case approvalData: + plugin, err := am.selectTokenPlugin(ctx, data.Pool.Connector) + if err != nil { + return false, err + } + return false, plugin.TokensApproval(ctx, op.ID, data.Pool.ProtocolID, data.Approval) + + 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}, + } +} + +func opApproval(op *fftypes.Operation, pool *fftypes.TokenPool, approval *fftypes.TokenApproval) *fftypes.PreparedOperation { + return &fftypes.PreparedOperation{ + ID: op.ID, + Type: op.Type, + Data: approvalData{Pool: pool, Approval: approval}, + } +} diff --git a/internal/assets/operations_test.go b/internal/assets/operations_test.go new file mode 100644 index 0000000000..adf6af3c20 --- /dev/null +++ b/internal/assets/operations_test.go @@ -0,0 +1,526 @@ +// 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 TestPrepareAndRunApproval(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenApproval, + } + pool := &fftypes.TokenPool{ + Connector: "magic-tokens", + ProtocolID: "F1", + } + approval := &fftypes.TokenApproval{ + LocalID: fftypes.NewUUID(), + Pool: pool.ID, + Approved: true, + } + txcommon.AddTokenApprovalInputs(op, approval) + + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mdi := am.database.(*databasemocks.Plugin) + mti.On("TokensApproval", context.Background(), op.ID, "F1", approval).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.(approvalData).Pool) + assert.Equal(t, approval, po.Data.(approvalData).Approval) + + 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, "FF10371", 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 TestPrepareOperationApprovalBadInput(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenApproval, + Input: fftypes.JSONObject{"localId": "bad"}, + } + + _, err := am.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10151", err) +} + +func TestPrepareOperationApprovalError(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + poolID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenApproval, + 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 TestPrepareOperationApprovalNotFound(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + poolID := fftypes.NewUUID() + op := &fftypes.Operation{ + Type: fftypes.OpTypeTokenApproval, + 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, "FF10371", 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 TestRunOperationApprovalBadPlugin(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + op := &fftypes.Operation{} + pool := &fftypes.TokenPool{} + approval := &fftypes.TokenApproval{} + + complete, err := am.RunOperation(context.Background(), opApproval(op, pool, approval)) + + 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/assets/token_approval.go b/internal/assets/token_approval.go index 8fd66a0550..255cacb694 100644 --- a/internal/assets/token_approval.go +++ b/internal/assets/token_approval.go @@ -127,13 +127,7 @@ func (s *approveSender) sendInternal(ctx context.Context, method sendMethod) err return err } - err = plugin.TokensApproval(ctx, op.ID, pool.ProtocolID, &s.approval.TokenApproval) - // if transaction fails, mark op as failed in DB - if err != nil { - s.mgr.txHelper.WriteOperationFailure(ctx, op.ID, err) - } - - return err + return s.mgr.operations.RunOperation(ctx, opApproval(op, pool, &s.approval.TokenApproval)) } func (am *assetManager) validateApproval(ctx context.Context, ns string, approval *fftypes.TokenApprovalInput) (err error) { diff --git a/internal/assets/token_approval_test.go b/internal/assets/token_approval_test.go index 096363c9f9..34cc39bc84 100644 --- a/internal/assets/token_approval_test.go +++ b/internal/assets/token_approval_test.go @@ -24,8 +24,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" @@ -64,17 +64,25 @@ func TestTokenApprovalSuccess(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) mim.On("NormalizeSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TokensApproval", context.Background(), mock.Anything, "F1", &approval.TokenApproval).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenApproval).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(approvalData) + return op.Type == fftypes.OpTypeTokenApproval && data.Pool == pool && data.Approval == &approval.TokenApproval + })).Return(nil) _, err := am.TokenApproval(context.Background(), "ns1", approval, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTokenApprovalSuccessUnknownIdentity(t *testing.T) { @@ -94,17 +102,25 @@ func TestTokenApprovalSuccessUnknownIdentity(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) mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TokensApproval", context.Background(), mock.Anything, "F1", &approval.TokenApproval).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenApproval).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(approvalData) + return op.Type == fftypes.OpTypeTokenApproval && data.Pool == pool && data.Approval == &approval.TokenApproval + })).Return(nil) _, err := am.TokenApproval(context.Background(), "ns1", approval, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestApprovalUnknownConnectorNoConnectors(t *testing.T) { @@ -122,9 +138,6 @@ func TestApprovalUnknownConnectorNoConnectors(t *testing.T) { am.tokens = make(map[string]tokens.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("NormalizeSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) - _, err := am.TokenApproval(context.Background(), "ns1", approval, false) assert.Regexp(t, "FF10292", err) } @@ -145,9 +158,6 @@ func TestApprovalUnknownConnectorMultipleConnectors(t *testing.T) { am.tokens["magic-tokens"] = nil am.tokens["magic-tokens2"] = nil - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("NormalizeSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) - _, err := am.TokenApproval(context.Background(), "ns1", approval, false) assert.Regexp(t, "FF10292", err) } @@ -167,9 +177,6 @@ func TestApprovalUnknownConnectorBadNamespace(t *testing.T) { am.tokens = make(map[string]tokens.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("NormalizeSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) - _, err := am.TokenApproval(context.Background(), "", approval, false) assert.Regexp(t, "FF10131", err) } @@ -208,9 +215,9 @@ func TestApprovalUnknownPoolSuccess(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) @@ -231,12 +238,20 @@ func TestApprovalUnknownPoolSuccess(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("TokensApproval", context.Background(), mock.Anything, "F1", &approval.TokenApproval).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenApproval).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(approvalData) + return op.Type == fftypes.OpTypeTokenApproval && data.Pool == tokenPools[0] && data.Approval == &approval.TokenApproval + })).Return(nil) _, err := am.TokenApproval(context.Background(), "ns1", approval, false) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestApprovalUnknownPoolNoPool(t *testing.T) { @@ -252,7 +267,6 @@ func TestApprovalUnknownPoolNoPool(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) @@ -261,7 +275,6 @@ func TestApprovalUnknownPoolNoPool(t *testing.T) { filterResult := &database.FilterResult{ TotalCount: &totalCount, } - mim.On("NormalizeSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) mdi.On("GetTokenPools", context.Background(), mock.MatchedBy((func(f database.AndFilter) bool { info, _ := f.Finalize() return info.Count && info.Limit == 1 @@ -358,18 +371,25 @@ func TestApprovalFail(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) mim.On("NormalizeSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TokensApproval", context.Background(), mock.Anything, "F1", &approval.TokenApproval).Return(fmt.Errorf("pop")) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenApproval).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("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(approvalData) + return op.Type == fftypes.OpTypeTokenApproval && data.Pool == pool && data.Approval == &approval.TokenApproval + })).Return(fmt.Errorf("pop")) _, err := am.TokenApproval(context.Background(), "ns1", approval, false) assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestApprovalTransactionFail(t *testing.T) { @@ -402,38 +422,6 @@ func TestApprovalTransactionFail(t *testing.T) { mdi.AssertExpectations(t) } -func TestApprovalFailAndDbFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - approval := &fftypes.TokenApprovalInput{ - TokenApproval: fftypes.TokenApproval{ - Approved: true, - Operator: "operator", - Key: "key", - }, - 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("NormalizeSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TokensApproval", context.Background(), mock.Anything, "F1", &approval.TokenApproval).Return(fmt.Errorf("pop")) - mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenApproval).Return(fftypes.NewUUID(), nil) - mth.On("WriteOperationFailure", context.Background(), mock.Anything, fmt.Errorf("pop")) - - _, err := am.TokenApproval(context.Background(), "ns1", approval, false) - assert.EqualError(t, err, "pop") -} - func TestApprovalOperationsFail(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() @@ -482,16 +470,19 @@ func TestTokenApprovalConfirm(t *testing.T) { } mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) mdm := am.data.(*datamocks.Manager) msa := am.syncasync.(*syncasyncmocks.Bridge) mth := am.txHelper.(*txcommonmocks.Helper) + mom := am.operations.(*operationmocks.Manager) mim.On("NormalizeSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mti.On("TokensApproval", context.Background(), mock.Anything, "F1", &approval.TokenApproval).Return(nil) mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenApproval).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) + mom.On("RunOperation", context.Background(), mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(approvalData) + return op.Type == fftypes.OpTypeTokenApproval && data.Pool == pool && data.Approval == &approval.TokenApproval + })).Return(nil) msa.On("WaitForTokenApproval", context.Background(), "ns1", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { @@ -504,9 +495,10 @@ func TestTokenApprovalConfirm(t *testing.T) { assert.NoError(t, err) mdi.AssertExpectations(t) + mim.AssertExpectations(t) mdm.AssertExpectations(t) msa.AssertExpectations(t) - mti.AssertExpectations(t) + mom.AssertExpectations(t) } func TestApprovalPrepare(t *testing.T) { diff --git a/internal/assets/token_pool.go b/internal/assets/token_pool.go index 108753e8b8..d4897bd2d1 100644 --- a/internal/assets/token_pool.go +++ b/internal/assets/token_pool.go @@ -79,24 +79,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.RunOperation(ctx, opCreatePool(op, pool)) } -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 @@ -107,17 +102,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.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 6635edaedd..7efa93dbc6 100644 --- a/internal/assets/token_pool_test.go +++ b/internal/assets/token_pool_test.go @@ -24,8 +24,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" @@ -57,17 +57,26 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("resolved-key", 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("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) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestCreateTokenPoolUnknownConnectorNoConnectors(t *testing.T) { @@ -85,6 +94,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) { @@ -103,6 +114,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) { @@ -113,27 +126,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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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) { @@ -150,6 +149,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) { @@ -167,6 +168,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) { @@ -178,18 +182,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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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) { @@ -203,18 +205,26 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("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) 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) { @@ -235,6 +245,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) { @@ -257,6 +271,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) { @@ -270,18 +289,26 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("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) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestCreateTokenPoolAsyncSuccess(t *testing.T) { @@ -295,17 +322,26 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("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) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestCreateTokenPoolConfirm(t *testing.T) { @@ -320,23 +356,33 @@ 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) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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) + 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("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) 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) { @@ -344,22 +390,30 @@ 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("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, ev) + err := am.ActivateTokenPool(context.Background(), pool, info) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mom.AssertExpectations(t) } func TestActivateTokenPoolBadConnector(t *testing.T) { @@ -370,12 +424,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) } @@ -387,17 +438,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) { @@ -408,21 +459,28 @@ 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("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, 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) { @@ -433,21 +491,28 @@ 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("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, 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) { @@ -458,6 +523,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) { @@ -468,6 +535,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) { @@ -478,6 +547,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) { @@ -513,6 +584,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) { @@ -532,6 +605,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) { @@ -543,6 +618,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) { @@ -553,6 +630,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) { @@ -571,6 +650,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) { @@ -584,6 +665,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 435d9172c6..2e26fbfa83 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" @@ -224,8 +223,8 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er return nil } - var pool *fftypes.TokenPool 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) if err != nil { @@ -242,6 +241,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, @@ -265,21 +265,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.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 b89c10749e..16b3653d44 100644 --- a/internal/assets/token_transfer_test.go +++ b/internal/assets/token_transfer_test.go @@ -25,10 +25,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" @@ -47,6 +47,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) { @@ -58,6 +60,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) { @@ -79,22 +83,29 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("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) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestMintTokenUnknownConnectorSuccess(t *testing.T) { @@ -108,22 +119,29 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("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) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestMintTokenUnknownConnectorNoConnectors(t *testing.T) { @@ -139,9 +157,6 @@ func TestMintTokenUnknownConnectorNoConnectors(t *testing.T) { am.tokens = make(map[string]tokens.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) - _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.Regexp(t, "FF10292", err) } @@ -160,9 +175,6 @@ func TestMintTokenUnknownConnectorMultipleConnectors(t *testing.T) { am.tokens["magic-tokens"] = nil am.tokens["magic-tokens2"] = nil - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) - _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.Regexp(t, "FF10292", err) } @@ -178,9 +190,6 @@ func TestMintTokenUnknownConnectorBadNamespace(t *testing.T) { Pool: "pool1", } - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) - _, err := am.MintTokens(context.Background(), "", mint, false) assert.Regexp(t, "FF10131", err) } @@ -202,6 +211,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) { @@ -215,17 +226,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) @@ -238,12 +248,20 @@ 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("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) assert.NoError(t, err) + + mdi.AssertExpectations(t) + mim.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestMintTokenUnknownPoolNoPools(t *testing.T) { @@ -257,7 +275,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) @@ -266,7 +283,6 @@ func TestMintTokenUnknownPoolNoPools(t *testing.T) { filterResult := &database.FilterResult{ TotalCount: &totalCount, } - mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) mdi.On("GetTokenPools", context.Background(), mock.MatchedBy((func(f database.AndFilter) bool { info, _ := f.Finalize() return info.Count && info.Limit == 1 @@ -274,6 +290,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) { @@ -287,7 +305,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) @@ -303,15 +320,15 @@ func TestMintTokenUnknownPoolMultiplePools(t *testing.T) { filterResult := &database.FilterResult{ TotalCount: &totalCount, } - mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) assert.Regexp(t, "FF10292", err) + + mdi.AssertExpectations(t) } func TestMintTokenUnknownPoolBadNamespace(t *testing.T) { @@ -324,9 +341,6 @@ func TestMintTokenUnknownPoolBadNamespace(t *testing.T) { }, } - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil) - _, err := am.MintTokens(context.Background(), "", mint, false) assert.Regexp(t, "FF10131", err) } @@ -342,12 +356,12 @@ func TestMintTokensGetPoolsError(t *testing.T) { } mdi := am.database.(*databasemocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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) { @@ -368,6 +382,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) { @@ -386,6 +403,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) { @@ -399,53 +418,29 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("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) 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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) { @@ -473,6 +468,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) { @@ -486,19 +485,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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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). @@ -507,6 +504,10 @@ func TestMintTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) + 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) assert.NoError(t, err) @@ -514,7 +515,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) { @@ -528,26 +529,29 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("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) assert.NoError(t, err) mim.AssertExpectations(t) mdi.AssertExpectations(t) - mti.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestBurnTokensIdentityFail(t *testing.T) { @@ -566,6 +570,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) { @@ -579,19 +585,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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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). @@ -600,6 +604,10 @@ func TestBurnTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) + 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) assert.NoError(t, err) @@ -607,7 +615,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) { @@ -623,26 +632,29 @@ 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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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("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) assert.NoError(t, err) mim.AssertExpectations(t) mdi.AssertExpectations(t) - mti.AssertExpectations(t) + mth.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferTokensUnconfirmedPool(t *testing.T) { @@ -692,6 +704,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) { @@ -711,40 +725,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() @@ -774,6 +754,7 @@ func TestTransferTokensTransactionFail(t *testing.T) { mim.AssertExpectations(t) mdi.AssertExpectations(t) + mth.AssertExpectations(t) } func TestTransferTokensWithBroadcastMessage(t *testing.T) { @@ -804,19 +785,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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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) @@ -824,6 +803,10 @@ 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("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) assert.NoError(t, err) @@ -833,8 +816,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) { @@ -901,19 +885,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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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) @@ -921,6 +903,10 @@ 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("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) assert.NoError(t, err) @@ -930,8 +916,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) { @@ -981,19 +968,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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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). @@ -1002,6 +987,10 @@ func TestTransferTokensConfirm(t *testing.T) { send(context.Background()) }). Return(&fftypes.TokenTransfer{}, nil) + 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) assert.NoError(t, err) @@ -1009,7 +998,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) { @@ -1040,20 +1031,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("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("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) @@ -1073,6 +1062,10 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { send(context.Background()) }). Return(&transfer.TokenTransfer, nil) + 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) assert.NoError(t, err) @@ -1082,9 +1075,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/batch/batch_processor.go b/internal/batch/batch_processor.go index 4d17900ff9..560cd8213b 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 6334abb127..d480f790e4 100644 --- a/internal/batchpin/batchpin.go +++ b/internal/batchpin/batchpin.go @@ -19,15 +19,23 @@ 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" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" ) type Submitter interface { + fftypes.Named + 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,39 +43,44 @@ 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(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, blockchain: bi, metrics: mm, + operations: om, } + om.RegisterHandler(ctx, bp, []fftypes.OpType{ + fftypes.OpTypeBlockchainBatchPin, + }) + return bp, nil } -func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error { +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( bp.blockchain, batch.Namespace, batch.Payload.TX.ID, fftypes.OpTypeBlockchainBatchPin) - if err := bp.database.InsertOperation(ctx, op); err != nil { + addBatchPinInputs(op, batch.ID, contexts) + if err := bp.operations.AddOrReuseOperation(ctx, op); err != nil { return err } 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 613b65b06e..ef3402ecde 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,21 +39,35 @@ 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) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) if enableMetrics { mmi.On("CountBatchPin").Return() } mbi.On("Name").Return("ut").Maybe() - bps := NewBatchPinSubmitter(mdi, mim, mbi, mmi).(*batchPinSubmitter) - return bps + bps, err := NewBatchPinSubmitter(context.Background(), mdi, mim, mbi, mmi, mom) + assert.NoError(t, err) + return bps.(*batchPinSubmitter) +} + +func TestInitFail(t *testing.T) { + _, err := NewBatchPinSubmitter(context.Background(), nil, nil, nil, nil, nil) + 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() - 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(), @@ -68,26 +83,34 @@ 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) 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(), SignerRef: fftypes.SignerRef{ @@ -102,25 +125,31 @@ 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) 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) { 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{ @@ -137,7 +166,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/batchpin/operations.go b/internal/batchpin/operations.go new file mode 100644 index 0000000000..80b481b868 --- /dev/null +++ b/internal/batchpin/operations.go @@ -0,0 +1,103 @@ +// 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 + } else if batch == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + } + 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/batchpin/operations_test.go b/internal/batchpin/operations_test.go new file mode 100644 index 0000000000..4ffa84ab6c --- /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(), + SignerRef: fftypes.SignerRef{ + 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, "FF10371", 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, "FF10371", 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.go b/internal/broadcast/manager.go index d2e0cf0943..e4f9a17d3a 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" @@ -41,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) @@ -51,6 +52,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 +71,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 sharedstorage.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, si sharedstorage.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 || si == nil || ba == nil || mm == nil || om == nil { return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) } bm := &broadcastManager{ @@ -79,19 +85,22 @@ func NewBroadcastManager(ctx context.Context, di database.Plugin, im identity.Ma data: dm, blockchain: bi, exchange: dx, - sharedstorage: pi, + sharedstorage: si, batch: ba, syncasync: sa, 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,51 +108,34 @@ 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.sharedstorage.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(ctx, bm, []fftypes.OpType{ + fftypes.OpTypeSharedStorageBatchBroadcast, }) -} -func (bm *broadcastManager) submitTXAndUpdateDB(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error { + return bm, nil +} - // 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 - } +func (bm *broadcastManager) Name() string { + return "BroadcastManager" +} +func (bm *broadcastManager) dispatchBatch(ctx context.Context, batch *fftypes.Batch, pins []*fftypes.Bytes32) error { // The completed SharedStorage upload op := fftypes.NewOperation( bm.sharedstorage, batch.Namespace, batch.Payload.TX.ID, fftypes.OpTypeSharedStorageBatchBroadcast) - 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.operations.AddOrReuseOperation(ctx, op); err != nil { + return err + } + if err := bm.operations.RunOperation(ctx, opBatchBroadcast(op, batch)); err != nil { return err } - log.L(ctx).Infof("Pinning broadcast batch %s with author=%s key=%s", batch.ID, batch.Author, batch.Key) - return bm.batchpin.SubmitPinnedBatch(ctx, batch, contexts) + 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 492774ec83..ef47cc8671 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/sharedstoragemocks" "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_sharedstorage").Maybe() @@ -64,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, mock.Anything) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { @@ -73,7 +76,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,10 +93,16 @@ 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) } +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() @@ -135,27 +144,38 @@ func TestBroadcastMessageBad(t *testing.T) { } -func TestDispatchBatchInvalidData(t *testing.T) { +func TestDispatchBatchInsertOpFail(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) + batch := &fftypes.Batch{} + + 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") + + mom.AssertExpectations(t) } func TestDispatchBatchUploadFail(t *testing.T) { bm, cancel := newTestBroadcast(t) defer cancel() - bm.sharedstorage.(*sharedstoragemocks.Plugin).On("PublishData", mock.Anything, mock.Anything).Return("", fmt.Errorf("pop")) - err := bm.dispatchBatch(context.Background(), &fftypes.Batch{}, []*fftypes.Bytes32{fftypes.NewRandB32()}) + batch := &fftypes.Batch{} + + mom := bm.operations.(*operationmocks.Manager) + 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.OpTypeSharedStorageBatchBroadcast && data.Batch == batch + })).Return(fmt.Errorf("pop")) + + err := bm.dispatchBatch(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) assert.EqualError(t, err, "pop") + + mom.AssertExpectations(t) } func TestDispatchBatchSubmitBatchPinSucceed(t *testing.T) { @@ -167,111 +187,45 @@ func TestDispatchBatchSubmitBatchPinSucceed(t *testing.T) { } mdi := bm.database.(*databasemocks.Plugin) - mps := bm.sharedstorage.(*sharedstoragemocks.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) - mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) + mom := bm.operations.(*operationmocks.Manager) + 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) + return op.Type == fftypes.OpTypeSharedStorageBatchBroadcast && 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{SignerRef: fftypes.SignerRef{Author: "wrong", Key: "wrong"}} + mdi := bm.database.(*databasemocks.Plugin) - mps := bm.sharedstorage.(*sharedstoragemocks.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) - mdi.On("InsertOperation", mock.Anything, mock.Anything).Return(nil) + mom := bm.operations.(*operationmocks.Manager) + 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) + return op.Type == fftypes.OpTypeSharedStorageBatchBroadcast && data.Batch == batch + })).Return(nil) - err := bm.dispatchBatch(context.Background(), &fftypes.Batch{SignerRef: fftypes.SignerRef{Author: "wrong", Key: "wrong"}}, []*fftypes.Bytes32{fftypes.NewRandB32()}) + err := bm.dispatchBatch(context.Background(), batch, []*fftypes.Bytes32{fftypes.NewRandB32()}) 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{SignerRef: fftypes.SignerRef{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{ - SignerRef: fftypes.SignerRef{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{ - SignerRef: fftypes.SignerRef{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_sharedstorage", op.Plugin) - assert.Equal(t, fftypes.OpTypeSharedStorageBatchBroadcast, op.Type) + mdi.AssertExpectations(t) + mbp.AssertExpectations(t) + mom.AssertExpectations(t) } func TestPublishBlobsUpdateDataFail(t *testing.T) { diff --git a/internal/broadcast/operations.go b/internal/broadcast/operations.go new file mode 100644 index 0000000000..372637ee9b --- /dev/null +++ b/internal/broadcast/operations.go @@ -0,0 +1,94 @@ +// 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.OpTypeSharedStorageBatchBroadcast: + id, err := retrieveBatchBroadcastInputs(ctx, op) + if err != nil { + return nil, err + } + 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 + + 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.sharedstorage.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/broadcast/operations_test.go b/internal/broadcast/operations_test.go new file mode 100644 index 0000000000..d5821283a6 --- /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/sharedstoragemocks" + "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.OpTypeSharedStorageBatchBroadcast, + } + batch := &fftypes.Batch{ + ID: fftypes.NewUUID(), + } + addBatchBroadcastInputs(op, batch.ID) + + mps := bm.sharedstorage.(*sharedstoragemocks.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, "FF10371", err) +} + +func TestPrepareOperationBatchBroadcastBadInput(t *testing.T) { + bm, cancel := newTestBroadcast(t) + defer cancel() + + op := &fftypes.Operation{ + Type: fftypes.OpTypeSharedStorageBatchBroadcast, + 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.OpTypeSharedStorageBatchBroadcast, + 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.OpTypeSharedStorageBatchBroadcast, + 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, "FF10371", 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.sharedstorage.(*sharedstoragemocks.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.sharedstorage.(*sharedstoragemocks.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/config/config.go b/internal/config/config.go index 03161b4273..0bc94c44ca 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -220,12 +220,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") // AssetManagerKeyNormalization mechanism to normalize keys before using them. Valid options: "blockchain_plugin" - use blockchain plugin (default), "none" - do not attempt normalization AssetManagerKeyNormalization = rootKey("asset.manager.keyNormalization") // UIEnabled set to false to disable the UI (default is true, so UI will be enabled if ui.path is valid) @@ -356,9 +350,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/contracts/manager.go b/internal/contracts/manager.go index 5710d9761d..0b6d737348 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" @@ -32,6 +33,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) @@ -51,6 +54,10 @@ type Manager interface { GetContractListeners(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.ContractListener, *database.FilterResult, error) DeleteContractListenerByNameOrID(ctx context.Context, ns, nameOrID string) error GenerateFFI(ctx context.Context, ns string, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, 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 { @@ -60,24 +67,37 @@ type contractManager struct { identity identity.Manager blockchain blockchain.Plugin ffiParamValidator fftypes.FFIParamValidator + operations operations.Manager } -func NewContractManager(ctx context.Context, database database.Plugin, broadcast broadcast.Manager, identity identity.Manager, blockchain blockchain.Plugin) (Manager, error) { - if database == nil || broadcast == nil || identity == nil || blockchain == nil { +func NewContractManager(ctx context.Context, di database.Plugin, bm broadcast.Manager, im identity.Manager, bi blockchain.Plugin, om operations.Manager) (Manager, error) { + if di == 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), - broadcast: broadcast, - identity: identity, - blockchain: blockchain, + + cm := &contractManager{ + database: di, + txHelper: txcommon.NewTransactionHelper(di), + broadcast: bm, + identity: im, + blockchain: bi, ffiParamValidator: v, - }, nil + operations: om, + } + + om.RegisterHandler(ctx, cm, []fftypes.OpType{ + fftypes.OpTypeBlockchainInvoke, + }) + + return cm, nil +} + +func (cm *contractManager) Name() string { + return "ContractManager" } func (cm *contractManager) newFFISchemaCompiler() *jsonschema.Compiler { @@ -156,7 +176,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 @@ -167,8 +187,10 @@ func (cm *contractManager) writeInvokeTransaction(ctx context.Context, ns string ns, txid, fftypes.OpTypeBlockchainInvoke) - op.Input = input - return op, cm.database.InsertOperation(ctx, op) + if err = addBlockchainInvokeInputs(op, req); err == nil { + err = cm.database.InsertOperation(ctx, op) + } + return op, err } func (cm *contractManager) InvokeContract(ctx context.Context, ns string, req *fftypes.ContractCallRequest) (res interface{}, err error) { @@ -186,7 +208,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 } @@ -199,18 +221,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 f67997f588..aa28114736 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -27,6 +27,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/txcommonmocks" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" @@ -40,7 +41,9 @@ func newTestContractManager() *contractManager { mbm := &broadcastmocks.Manager{} mim := &identitymanagermocks.Manager{} mbi := &blockchainmocks.Plugin{} + mom := &operationmocks.Manager{} mbi.On("GetFFIParamValidator", mock.Anything).Return(nil, nil) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) mbi.On("Name").Return("mockblockchain").Maybe() @@ -50,23 +53,29 @@ func newTestContractManager() *contractManager { a[1].(func(context.Context) error)(a[0].(context.Context)), } } - cm, _ := NewContractManager(context.Background(), mdb, mbm, mim, mbi) + cm, _ := NewContractManager(context.Background(), mdb, 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) + _, err := NewContractManager(context.Background(), nil, nil, nil, nil, nil) 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{} 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, mbm, mim, mbi) + _, err := NewContractManager(context.Background(), mdb, mbm, mim, mbi, mom) assert.Regexp(t, "pop", err) } @@ -75,8 +84,10 @@ 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, mbm, mim, mbi) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) + _, err := NewContractManager(context.Background(), mdb, mbm, mim, mbi, mom) assert.NoError(t, err) } @@ -1116,10 +1127,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, @@ -1135,25 +1146,31 @@ func TestInvokeContract(t *testing.T) { } mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(fftypes.NewUUID(), nil) - mim.On("NormalizeSigningKey", mock.Anything, "", identity.KeyNormalizationBlockchainPlugin).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, @@ -1169,18 +1186,23 @@ func TestInvokeContractFail(t *testing.T) { } mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(fftypes.NewUUID(), nil) - mim.On("NormalizeSigningKey", mock.Anything, "", identity.KeyNormalizationBlockchainPlugin).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 TestInvokeContractFailNormalizeSigningKey(t *testing.T) { @@ -1505,9 +1527,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, @@ -1533,11 +1555,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/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/contracts/operations_test.go b/internal/contracts/operations_test.go new file mode 100644 index 0000000000..29715bffc6 --- /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, "FF10371", 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, "FF10371", err) +} 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/definitions/definition_handler_tokenpool.go b/internal/definitions/definition_handler_tokenpool.go index 1def4a9048..eb40243b91 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 } @@ -67,6 +64,7 @@ func (dh *definitionHandlers) handleTokenPoolBroadcast(ctx context.Context, stat return HandlerResult{Action: ActionConfirm, CustomCorrelator: correlator}, nil } + // Create the pool in pending state if valid, err := dh.persistTokenPool(ctx, &announce); err != nil { return HandlerResult{Action: ActionRetry}, err } else if !valid { @@ -76,7 +74,7 @@ func (dh *definitionHandlers) handleTokenPoolBroadcast(ctx context.Context, stat // Message will remain unconfirmed, but plugin will be notified to activate the pool // This will ultimately trigger a pool creation event and a rewind state.AddPreFinalize(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 2d466a5258..55cbb64ff1 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, err := sh.HandleDefinitionBroadcast(context.Background(), bs, msg, data, fftypes.NewUUID()) assert.Equal(t, HandlerResult{Action: ActionWait, CustomCorrelator: pool.ID}, action) @@ -125,7 +127,7 @@ func TestHandleDefinitionBroadcastTokenPoolExisting(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, err := sh.HandleDefinitionBroadcast(context.Background(), bs, msg, data, fftypes.NewUUID()) assert.Equal(t, HandlerResult{Action: ActionWait, CustomCorrelator: pool.ID}, action) @@ -215,7 +217,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, err := sh.HandleDefinitionBroadcast(context.Background(), bs, msg, data, fftypes.NewUUID()) assert.Equal(t, HandlerResult{Action: ActionWait, CustomCorrelator: pool.ID}, action) diff --git a/internal/events/operation_update.go b/internal/events/operation_update.go index a2be9cecfc..0fb7af2fab 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 } @@ -38,14 +38,13 @@ func (em *eventManager) operationUpdateCtx(ctx context.Context, operationID *fft // Special handling for OpTypeTokenTransfer, which writes an event when it fails if op.Type == fftypes.OpTypeTokenTransfer && txState == fftypes.OpStatusFailed { event := fftypes.NewEvent(fftypes.EventTypeTransferOpFailed, op.Namespace, op.ID, op.Transaction) - var tokenTransfer fftypes.TokenTransfer - err = txcommon.RetrieveTokenTransferInputs(ctx, op, &tokenTransfer) - if err != nil { - log.L(em.ctx).Warnf("Could not determine token transfer: %s", err) + tokenTransfer, err := txcommon.RetrieveTokenTransferInputs(ctx, op) + if err != nil || tokenTransfer.LocalID == nil || tokenTransfer.Type == "" { + log.L(em.ctx).Warnf("Could not parse token transfer: %s", err) } else { event.Correlator = tokenTransfer.LocalID if em.metrics.IsMetricsEnabled() { - em.metrics.TransferConfirmed(&tokenTransfer) + em.metrics.TransferConfirmed(tokenTransfer) } } if err := em.database.InsertEvent(ctx, event); err != nil { @@ -56,10 +55,9 @@ func (em *eventManager) operationUpdateCtx(ctx context.Context, operationID *fft // Special handling for OpTypeTokenApproval, which writes an event when it fails if op.Type == fftypes.OpTypeTokenApproval && txState == fftypes.OpStatusFailed { event := fftypes.NewEvent(fftypes.EventTypeApprovalOpFailed, op.Namespace, op.ID, op.Transaction) - var tokenApproval fftypes.TokenApproval - err = txcommon.RetrieveTokenApprovalInputs(ctx, op, &tokenApproval) - if err != nil { - log.L(em.ctx).Warnf("Could not determine token retrieval: %s", err) + tokenApproval, err := txcommon.RetrieveTokenApprovalInputs(ctx, op) + if err != nil || tokenApproval.LocalID == nil { + log.L(em.ctx).Warnf("Could not parse token approval: %s", err) } else { event.Correlator = tokenApproval.LocalID } diff --git a/internal/events/operation_update_test.go b/internal/events/operation_update_test.go index 0451d94892..13b62606fe 100644 --- a/internal/events/operation_update_test.go +++ b/internal/events/operation_update_test.go @@ -154,6 +154,7 @@ func TestOperationUpdateTransferFail(t *testing.T) { Transaction: fftypes.NewUUID(), Input: fftypes.JSONObject{ "localId": localID.String(), + "type": "transfer", }, } info := fftypes.JSONObject{"some": "info"} diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go index 5a04baf905..b5b4cf9957 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 5a28068a07..70ed361d33 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_approved.go b/internal/events/tokens_approved.go index 931d7ccbd0..a6bccb029d 100644 --- a/internal/events/tokens_approved.go +++ b/internal/events/tokens_approved.go @@ -40,8 +40,10 @@ func (em *eventManager) loadApprovalOperation(ctx context.Context, tx *fftypes.U return err } if len(operations) > 0 { - if err = txcommon.RetrieveTokenApprovalInputs(ctx, operations[0], approval); err != nil { + if origApproval, err := txcommon.RetrieveTokenApprovalInputs(ctx, operations[0]); err != nil { log.L(ctx).Warnf("Failed to read operation inputs for token approval '%s': %s", approval.ProtocolID, err) + } else if origApproval != nil { + approval.LocalID = origApproval.LocalID } } diff --git a/internal/events/tokens_transferred.go b/internal/events/tokens_transferred.go index 453364c05a..a7444e2a1c 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 f08d36f953..de93364dde 100644 --- a/internal/i18n/en_translations.go +++ b/internal/i18n/en_translations.go @@ -287,4 +287,5 @@ var ( MsgNilOrNullObject = ffm("FF10368", "Object is null") MsgTokenApprovalFailed = ffm("FF10369", "Token approval with ID '%s' failed. Please check the FireFly logs for more information") MsgEventNotFound = ffm("FF10370", "Event with name '%s' not found", 400) + MsgOperationNotSupported = ffm("FF10371", "Operation not supported", 400) ) 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 new file mode 100644 index 0000000000..6b774543a9 --- /dev/null +++ b/internal/operations/manager.go @@ -0,0 +1,150 @@ +// 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" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/internal/log" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/hyperledger/firefly/pkg/tokens" +) + +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(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) + AddOrReuseOperation(ctx context.Context, op *fftypes.Operation) error +} + +type operationsManager struct { + ctx context.Context + database database.Plugin + 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) { + if di == nil || ti == nil { + return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) + } + om := &operationsManager{ + ctx: ctx, + database: di, + tokens: ti, + handlers: make(map[fftypes.OpType]OperationHandler), + } + return om, nil +} + +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 + } +} + +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 handler.PrepareOperation(ctx, op) +} + +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) + } + 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 + } else if complete { + om.writeOperationSuccess(ctx, op.ID) + } + 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.findLatestRetry(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.Error = "" + 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) + } +} + +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 new file mode 100644 index 0000000000..5820a09605 --- /dev/null +++ b/internal/operations/manager_test.go @@ -0,0 +1,365 @@ +// 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/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" + "github.com/stretchr/testify/mock" +) + +type mockHandler struct { + Complete bool + Err error + 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 +} + +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{} + 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}) + assert.NoError(t, err) + 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() + + op := &fftypes.Operation{} + + _, err := om.PrepareOperation(context.Background(), op) + assert.Regexp(t, "FF10371", err) +} + +func TestPrepareOperationSuccess(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + op := &fftypes.Operation{ + Type: fftypes.OpTypeBlockchainBatchPin, + } + + om.RegisterHandler(ctx, &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, "FF10371", err) +} + +func TestRunOperationSuccess(t *testing.T) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + op := &fftypes.PreparedOperation{ + Type: fftypes.OpTypeBlockchainBatchPin, + } + + om.RegisterHandler(ctx, &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(ctx, &mockHandler{Complete: true}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + err := om.RunOperation(ctx, op) + + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +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(ctx, &mockHandler{Err: fmt.Errorf("pop")}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + 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(ctx, &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(ctx, &mockHandler{Prepared: po}, []fftypes.OpType{fftypes.OpTypeBlockchainBatchPin}) + _, err := om.RetryOperation(ctx, "ns1", op.ID) + + assert.EqualError(t, err, "pop") + + 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(ctx, &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(ctx, &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(ctx, &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) { + om, cancel := newTestOperations(t) + defer cancel() + + ctx := context.Background() + opID := fftypes.NewUUID() + + mdi := om.database.(*databasemocks.Plugin) + mdi.On("ResolveOperation", ctx, opID, fftypes.OpStatusSucceeded, "", mock.Anything).Return(fmt.Errorf("pop")) + + om.writeOperationSuccess(ctx, opID) + + 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/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index aabe12c060..1492f40cde 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/sharedstorage/ssfactory" "github.com/hyperledger/firefly/internal/syncasync" @@ -76,6 +77,7 @@ type Orchestrator interface { Contracts() contracts.Manager Metrics() metrics.Manager BatchManager() batch.Manager + Operations() operations.Manager IsPreInit() bool // Status @@ -160,6 +162,7 @@ type orchestrator struct { contracts contracts.Manager node *fftypes.UUID metrics metrics.Manager + operations operations.Manager } func NewOrchestrator() Orchestrator { @@ -284,6 +287,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) @@ -458,30 +465,41 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) { } } + 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) + + 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); 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.sharedstorage, 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.sharedstorage, or.batch, or.syncasync, or.batchpin, or.metrics, or.operations); err != nil { return err } } 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 } } if or.contracts == nil { - or.contracts, err = contracts.NewContractManager(ctx, or.database, or.broadcast, or.identity, or.blockchain) + or.contracts, err = contracts.NewContractManager(ctx, or.database, or.broadcast, or.identity, or.blockchain, or.operations) if err != nil { return err } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index e2a517667e..7ca4ebb6b2 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" @@ -38,6 +39,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/sharedstoragemocks" "github.com/hyperledger/firefly/mocks/tokenmocks" @@ -68,6 +70,8 @@ type testOrchestrator struct { mti *tokenmocks.Plugin mcm *contractmocks.Manager mmi *metricsmocks.Manager + mom *operationmocks.Manager + mbp *batchpinmocks.Submitter } func newTestOrchestrator() *testOrchestrator { @@ -94,6 +98,8 @@ func newTestOrchestrator() *testOrchestrator { mti: &tokenmocks.Plugin{}, mcm: &contractmocks.Manager{}, mmi: &metricsmocks.Manager{}, + mom: &operationmocks.Manager{}, + mbp: &batchpinmocks.Submitter{}, } tor.orchestrator.database = tor.mdi tor.orchestrator.data = tor.mdm @@ -111,6 +117,8 @@ 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.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() @@ -524,6 +532,22 @@ 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 + or.operations = nil + err := or.initComponents(context.Background()) + assert.Regexp(t, "FF10128", err) +} + func TestStartBatchFail(t *testing.T) { config.Reset() or := newTestOrchestrator() @@ -656,6 +680,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/internal/privatemessaging/message_test.go b/internal/privatemessaging/message_test.go index 5901e14bf8..f643ca9c5f 100644 --- a/internal/privatemessaging/message_test.go +++ b/internal/privatemessaging/message_test.go @@ -26,6 +26,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" @@ -685,49 +686,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("ResolveInputSigningIdentity", pm.ctx, "ns1", mock.MatchedBy(func(identity *fftypes.SignerRef) bool { - assert.Equal(t, "localorg", identity.Author) - return true - })).Return(nil) - - groupID := fftypes.NewRandB32() - node1 := newTestNode("node1", newTestOrg("localorg")) - node2 := newTestNode("node2", newTestOrg("remoteorg")) - - mdi := pm.database.(*databasemocks.Plugin) - mdi.On("GetGroupByHash", pm.ctx, groupID).Return(&fftypes.Group{ - Hash: groupID, - GroupIdentity: fftypes.GroupIdentity{ - Members: fftypes.Members{ - {Node: node1.ID, Identity: "localorg"}, - {Node: node1.ID, Identity: "remoteorg"}, - }, - }, - }, nil).Once() - mdi.On("GetIdentityByID", pm.ctx, node1.ID).Return(node1, nil).Once() - mdi.On("GetIdentityByID", pm.ctx, node1.ID).Return(node2, 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) @@ -749,6 +707,7 @@ func TestDispatchedUnpinnedMessageOK(t *testing.T) { mdx.On("SendMessage", pm.ctx, mock.Anything, "node2-peer", mock.Anything).Return(nil) mdi := pm.database.(*databasemocks.Plugin) + mom := pm.operations.(*operationmocks.Manager) mdi.On("GetGroupByHash", pm.ctx, groupID).Return(&fftypes.Group{ Hash: groupID, GroupIdentity: fftypes.GroupIdentity{ @@ -761,7 +720,11 @@ func TestDispatchedUnpinnedMessageOK(t *testing.T) { mdi.On("GetIdentityByID", pm.ctx, node1.ID).Return(node1, nil).Once() mdi.On("GetIdentityByID", pm.ctx, node2.ID).Return(node2, 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 == *node2.ID + })).Return(nil) err := pm.dispatchUnpinnedBatch(pm.ctx, &fftypes.Batch{ ID: fftypes.NewUUID(), @@ -787,6 +750,7 @@ func TestDispatchedUnpinnedMessageOK(t *testing.T) { assert.NoError(t, err) mdi.AssertExpectations(t) + mom.AssertExpectations(t) } @@ -851,17 +815,14 @@ func TestSendDataTransferFail(t *testing.T) { nodes := []*fftypes.Identity{node2} mim := pm.identity.(*identitymanagermocks.Manager) - mim.On("ResolveInputIdentity", pm.ctx, mock.MatchedBy(func(identity *fftypes.SignerRef) bool { - assert.Equal(t, "localorg", identity.Author) - return true - })).Return(nil) mim.On("GetNodeOwnerOrg", 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, "node2-peer", mock.Anything).Return(fmt.Errorf("pop")) + 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 == *node2.ID + })).Return(fmt.Errorf("pop")) err := pm.sendData(pm.ctx, &fftypes.TransportWrapper{ Batch: &fftypes.Batch{ @@ -884,7 +845,8 @@ func TestSendDataTransferFail(t *testing.T) { }, nodes) assert.Regexp(t, "pop", err) - mdx.AssertExpectations(t) + mim.AssertExpectations(t) + mom.AssertExpectations(t) } @@ -905,8 +867,8 @@ func TestSendDataTransferInsertOperationFail(t *testing.T) { })).Return(nil) mim.On("GetNodeOwnerOrg", 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")) err := pm.sendData(pm.ctx, &fftypes.TransportWrapper{ Batch: &fftypes.Batch{ diff --git a/internal/privatemessaging/operations.go b/internal/privatemessaging/operations.go new file mode 100644 index 0000000000..38fa052698 --- /dev/null +++ b/internal/privatemessaging/operations.go @@ -0,0 +1,158 @@ +// 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.Identity `json:"node"` + Blob *fftypes.Blob `json:"blob"` +} + +type batchSendData struct { + Node *fftypes.Identity `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(), + "hash": 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("hash")) + } + return nodeID, blobHash, err +} + +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, + } +} + +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 { + groupHash, err = fftypes.ParseBytes32(ctx, op.Input.GetString("group")) + } + if err == nil { + batchID, err = fftypes.ParseUUID(ctx, op.Input.GetString("batch")) + } + if err == nil { + manifest = op.Input.GetString("manifest") + } + return nodeID, groupHash, batchID, manifest, err +} + +func (pm *privateMessaging) PrepareOperation(ctx context.Context, op *fftypes.Operation) (*fftypes.PreparedOperation, error) { + switch op.Type { + case fftypes.OpTypeDataExchangeBlobSend: + nodeID, blobHash, err := retrieveTransferBlobInputs(ctx, op) + if err != nil { + return nil, err + } + node, err := pm.database.GetIdentityByID(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 + + case fftypes.OpTypeDataExchangeBatchSend: + nodeID, groupHash, batchID, _, err := retrieveBatchSendInputs(ctx, op) + if err != nil { + return nil, err + } + node, err := pm.database.GetIdentityByID(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 + + 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.Profile.GetString("id"), 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.Profile.GetString("id"), payload) + + default: + return false, i18n.NewError(ctx, i18n.MsgOperationNotSupported) + } +} + +func opTransferBlob(op *fftypes.Operation, node *fftypes.Identity, 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.Identity, 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/operations_test.go b/internal/privatemessaging/operations_test.go new file mode 100644 index 0000000000..82423e5496 --- /dev/null +++ b/internal/privatemessaging/operations_test.go @@ -0,0 +1,480 @@ +// 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.Identity{ + IdentityBase: fftypes.IdentityBase{ + ID: fftypes.NewUUID(), + }, + IdentityProfile: fftypes.IdentityProfile{ + Profile: fftypes.JSONObject{ + "id": "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("GetIdentityByID", 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.Identity{ + IdentityBase: fftypes.IdentityBase{ + ID: fftypes.NewUUID(), + }, + IdentityProfile: fftypes.IdentityProfile{ + Profile: fftypes.JSONObject{ + "id": "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("GetIdentityByID", 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, "FF10371", 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(), + "hash": blobHash.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetIdentityByID", 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(), + "hash": blobHash.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetIdentityByID", 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.Identity{ + IdentityBase: fftypes.IdentityBase{ + ID: fftypes.NewUUID(), + }, + IdentityProfile: fftypes.IdentityProfile{ + Profile: fftypes.JSONObject{ + "id": "peer1", + }, + }, + } + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBlobSend, + Input: fftypes.JSONObject{ + "node": node.ID.String(), + "hash": blobHash.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetIdentityByID", 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.Identity{ + IdentityBase: fftypes.IdentityBase{ + ID: fftypes.NewUUID(), + }, + IdentityProfile: fftypes.IdentityProfile{ + Profile: fftypes.JSONObject{ + "id": "peer1", + }, + }, + } + op := &fftypes.Operation{ + Type: fftypes.OpTypeDataExchangeBlobSend, + Input: fftypes.JSONObject{ + "node": node.ID.String(), + "hash": blobHash.String(), + }, + } + + mdi := pm.database.(*databasemocks.Plugin) + mdi.On("GetIdentityByID", 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("GetIdentityByID", 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("GetIdentityByID", 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.Identity{ + IdentityBase: fftypes.IdentityBase{ + 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("GetIdentityByID", 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.Identity{ + IdentityBase: fftypes.IdentityBase{ + 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("GetIdentityByID", 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.Identity{ + IdentityBase: fftypes.IdentityBase{ + 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("GetIdentityByID", 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.Identity{ + IdentityBase: fftypes.IdentityBase{ + 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("GetIdentityByID", 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, "FF10371", err) +} + +func TestRunOperationBatchSendInvalidData(t *testing.T) { + pm, cancel := newTestPrivateMessaging(t) + defer cancel() + + op := &fftypes.Operation{} + node := &fftypes.Identity{ + IdentityBase: fftypes.IdentityBase{ + 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) +} diff --git a/internal/privatemessaging/privatemessaging.go b/internal/privatemessaging/privatemessaging.go index 2d30393070..e38bd92fb8 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" @@ -42,12 +42,17 @@ const pinnedPrivateDispatcherName = "pinned_private" const unpinnedPrivateDispatcherName = "unpinned_private" type Manager interface { + fftypes.Named GroupManager Start() error 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 +73,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 +105,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,9 +136,18 @@ func NewPrivateMessaging(ctx context.Context, di database.Plugin, im identity.Ma }, pm.dispatchUnpinnedBatch, bo) + om.RegisterHandler(ctx, pm, []fftypes.OpType{ + fftypes.OpTypeDataExchangeBlobSend, + fftypes.OpTypeDataExchangeBatchSend, + }) + return pm, nil } +func (pm *privateMessaging) Name() string { + return "PrivateMessaging" +} + func (pm *privateMessaging) Start() error { return pm.exchange.Start() } @@ -188,16 +204,12 @@ func (pm *privateMessaging) transferBlobs(ctx context.Context, data []*fftypes.D d.Namespace, txid, fftypes.OpTypeDataExchangeBlobSend) - op.Input = fftypes.JSONObject{ - "hash": d.Blob.Hash, - } - if err = pm.database.InsertOperation(ctx, op); err != nil { + addTransferBlobInputs(op, node.ID, blob.Hash) + if err = pm.operations.AddOrReuseOperation(ctx, op); err != nil { return err } - if err := pm.exchange.TransferBLOB(ctx, op.ID, node.Profile.GetString("id"), blob.PayloadRef); err != nil { - return err - } + return pm.operations.RunOperation(ctx, opTransferBlob(op, node, blob)) } } return nil @@ -207,11 +219,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) - } - // Lookup the local org localOrg, err := pm.identity.GetNodeOwnerOrg(ctx) if err != nil { @@ -238,16 +245,15 @@ 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 { + addBatchSendInputs(op, node.ID, groupHash, batch.ID, tw.Batch.Manifest().String()) + if err = pm.operations.AddOrReuseOperation(ctx, op); err != nil { return err } - - // Send the payload itself - err := pm.exchange.SendMessage(ctx, op.ID, node.Profile.GetString("id"), payload) - if err != nil { + if err = pm.operations.RunOperation(ctx, opBatchSend(op, node, tw)); err != nil { return err } } diff --git a/internal/privatemessaging/privatemessaging_test.go b/internal/privatemessaging/privatemessaging_test.go index bc88e93adf..0cde3fa3d3 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, @@ -68,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, mock.Anything) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { @@ -77,7 +80,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 @@ -98,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) @@ -119,12 +128,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.SignerRef) - assert.Equal(t, "org1", identity.Author) - identity.Key = "0x12345" - }).Return(nil) mim.On("GetNodeOwnerOrg", pm.ctx).Return(localOrg, nil) mdi.On("GetGroupByHash", pm.ctx, groupID).Return(&fftypes.Group{ Hash: fftypes.NewRandB32(), @@ -142,15 +147,32 @@ func TestDispatchBatchWithBlobs(t *testing.T) { Hash: blob1, PayloadRef: "/blob/1", }, nil) - mdx.On("TransferBLOB", pm.ctx, mock.Anything, "node2-peer", "/blob/1").Return(nil).Once() - 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) - - 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 { + mom.On("AddOrReuseOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeDataExchangeBlobSend + })).Return(nil, nil) + mom.On("AddOrReuseOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Type == fftypes.OpTypeDataExchangeBatchSend })).Return(nil, nil) + 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 { + if op.Type != fftypes.OpTypeDataExchangeBlobSend { + return false + } + data := op.Data.(transferBlobData) + return *data.Node.ID == *node2.ID + })).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.ID + })).Return(nil) mbp.On("SubmitPinnedBatch", pm.ctx, mock.Anything, mock.Anything).Return(nil) @@ -174,33 +196,17 @@ 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) { - _, 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) } -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() @@ -311,7 +317,8 @@ func TestSendSubmitInsertOperationFail(t *testing.T) { }, }, }, nil) - 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{ Group: groupID, @@ -356,14 +363,18 @@ func TestSendSubmitBlobTransferFail(t *testing.T) { }, }, }, nil) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) + + mom := pm.operations.(*operationmocks.Manager) + mom.On("AddOrReuseOperation", pm.ctx, mock.Anything).Return(nil) mdi.On("GetBlobMatchingHash", pm.ctx, blob1).Return(&fftypes.Blob{ Hash: blob1, PayloadRef: "/blob/1", }, nil) - mdx := pm.exchange.(*dataexchangemocks.Plugin) - mdx.On("TransferBLOB", pm.ctx, mock.Anything, "node2-peer", "/blob/1").Return(fmt.Errorf("pop")).Once() + mom.On("RunOperation", pm.ctx, mock.MatchedBy(func(op *fftypes.PreparedOperation) bool { + data := op.Data.(transferBlobData) + return op.Type == fftypes.OpTypeDataExchangeBlobSend && *data.Node.ID == *node2.ID + })).Return(fmt.Errorf("pop")) err := pm.dispatchPinnedBatch(pm.ctx, &fftypes.Batch{ Group: groupID, @@ -380,7 +391,7 @@ func TestSendSubmitBlobTransferFail(t *testing.T) { mdi.AssertExpectations(t) mim.AssertExpectations(t) - mdx.AssertExpectations(t) + mom.AssertExpectations(t) } func TestWriteTransactionSubmitBatchPinFail(t *testing.T) { @@ -409,16 +420,29 @@ func TestWriteTransactionSubmitBatchPinFail(t *testing.T) { }, }, }, nil) - 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 { + if op.Type != fftypes.OpTypeDataExchangeBlobSend { + return false + } + data := op.Data.(transferBlobData) + return *data.Node.ID == *node2.ID + })).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.ID + })).Return(nil) + mdi.On("GetBlobMatchingHash", pm.ctx, blob1).Return(&fftypes.Blob{ Hash: blob1, PayloadRef: "/blob/1", }, nil) - mdx := pm.exchange.(*dataexchangemocks.Plugin) - mdx.On("TransferBLOB", pm.ctx, mock.Anything, "node2-peer", "/blob/1").Return(nil).Once() - mdx.On("SendMessage", pm.ctx, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - mbp := pm.batchpin.(*batchpinmocks.Submitter) mbp.On("SubmitPinnedBatch", pm.ctx, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) @@ -437,8 +461,8 @@ func TestWriteTransactionSubmitBatchPinFail(t *testing.T) { mdi.AssertExpectations(t) mim.AssertExpectations(t) - mdx.AssertExpectations(t) mbp.AssertExpectations(t) + mom.AssertExpectations(t) } func TestTransferBlobsNotFound(t *testing.T) { @@ -456,33 +480,17 @@ func TestTransferBlobsNotFound(t *testing.T) { mdi.AssertExpectations(t) } -func TestTransferBlobsFail(t *testing.T) { - pm, cancel := newTestPrivateMessaging(t) - defer cancel() - - mdi := pm.database.(*databasemocks.Plugin) - 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, "node1-peer", "blob/1").Return(fmt.Errorf("pop")) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(nil) - - err := pm.transferBlobs(pm.ctx, []*fftypes.Data{ - {ID: fftypes.NewUUID(), Hash: fftypes.NewRandB32(), Blob: &fftypes.BlobRef{Hash: fftypes.NewRandB32()}}, - }, fftypes.NewUUID(), newTestNode("node1", newTestOrg("org1"))) - assert.Regexp(t, "pop", err) - - mdi.AssertExpectations(t) - mdx.AssertExpectations(t) -} - func TestTransferBlobsOpInsertFail(t *testing.T) { pm, cancel := newTestPrivateMessaging(t) defer cancel() 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) - mdi.On("InsertOperation", pm.ctx, mock.Anything).Return(fmt.Errorf("pop")) + mdx.On("TransferBLOB", pm.ctx, mock.Anything, "peer1", "blob/1").Return(nil) + 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/internal/syncasync/sync_async_bridge.go b/internal/syncasync/sync_async_bridge.go index 5181535701..a4cd6f344e 100644 --- a/internal/syncasync/sync_async_bridge.go +++ b/internal/syncasync/sync_async_bridge.go @@ -377,8 +377,8 @@ func (sa *syncAsyncBridge) handleTransferOpFailedEvent(event *fftypes.EventDeliv 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) } @@ -399,8 +399,8 @@ func (sa *syncAsyncBridge) handleApprovalOpFailedEvent(event *fftypes.EventDeliv return err } // Extract the LocalID of the transfer - var approval fftypes.TokenApproval - if err := txcommon.RetrieveTokenApprovalInputs(sa.ctx, op, &approval); err != nil { + approval, err := txcommon.RetrieveTokenApprovalInputs(sa.ctx, op) + if err != nil || approval.LocalID == nil { log.L(sa.ctx).Warnf("Failed to extract token approval inputs for operation '%s': %s", op.ID, err) } diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go index 3f6337483a..2336d26f37 100644 --- a/internal/tokens/fftokens/fftokens.go +++ b/internal/tokens/fftokens/fftokens.go @@ -473,12 +473,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 3de499bcb4..366cab0325 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 d58528ccba..a63ed202d2 100644 --- a/internal/txcommon/token_inputs.go +++ b/internal/txcommon/token_inputs.go @@ -19,59 +19,56 @@ 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 } func AddTokenApprovalInputs(op *fftypes.Operation, approval *fftypes.TokenApproval) (err error) { @@ -82,17 +79,11 @@ func AddTokenApprovalInputs(op *fftypes.Operation, approval *fftypes.TokenApprov return err } -func RetrieveTokenApprovalInputs(ctx context.Context, op *fftypes.Operation, approval *fftypes.TokenApproval) (err error) { - var a fftypes.TokenApproval +func RetrieveTokenApprovalInputs(ctx context.Context, op *fftypes.Operation) (approval *fftypes.TokenApproval, err error) { + var approve fftypes.TokenApproval s := op.Input.String() - if err = json.Unmarshal([]byte(s), &a); err != nil { - return i18n.WrapError(ctx, err, i18n.MsgJSONObjectParseFailed, s) - } - if a.LocalID == nil { - return i18n.NewError(ctx, i18n.MsgInvalidUUID) + if err = json.Unmarshal([]byte(s), &approve); 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) - approval.LocalID = a.LocalID - return nil + return &approve, nil } diff --git a/internal/txcommon/token_inputs_test.go b/internal/txcommon/token_inputs_test.go index 65633b965a..f8bd8b4214 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,22 +155,11 @@ 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) -} - func TestAddTokenApprovalInputs(t *testing.T) { op := &fftypes.Operation{} approval := &fftypes.TokenApproval{ @@ -187,13 +190,13 @@ func TestRetrieveTokenApprovalInputs(t *testing.T) { id := fftypes.NewUUID() op := &fftypes.Operation{ Input: fftypes.JSONObject{ - "amount": "1", - "localId": id.String(), + "amount": "1", + "localId": id.String(), + "approved": true, }, } - approval := &fftypes.TokenApproval{Approved: true} - err := RetrieveTokenApprovalInputs(context.Background(), op, approval) + approval, err := RetrieveTokenApprovalInputs(context.Background(), op) assert.NoError(t, err) assert.Equal(t, *id, *approval.LocalID) assert.Equal(t, true, approval.Approved) @@ -205,18 +208,7 @@ func TestRetrieveTokenApprovalInputsBadID(t *testing.T) { "localId": "bad", }, } - approval := &fftypes.TokenApproval{} - err := RetrieveTokenApprovalInputs(context.Background(), op, approval) + _, err := RetrieveTokenApprovalInputs(context.Background(), op) assert.Regexp(t, "FF10151", err) } - -func TestRetrieveTokenApprovalInputsMissingID(t *testing.T) { - op := &fftypes.Operation{ - Input: fftypes.JSONObject{}, - } - approval := &fftypes.TokenApproval{} - - err := RetrieveTokenApprovalInputs(context.Background(), op, approval) - assert.Regexp(t, "FF10142", err) -} diff --git a/internal/txcommon/txcommon.go b/internal/txcommon/txcommon.go index 05f4fdb3f8..733b80f03e 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) - -} diff --git a/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index 83ed285533..b2618dd2df 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) } @@ -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) @@ -417,18 +431,48 @@ 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() +// 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 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() + var r0 *fftypes.PreparedOperation + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Operation) *fftypes.PreparedOperation); ok { + r0 = rf(ctx, op) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.PreparedOperation) + } } - return r0 + 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 } // TokenApproval provides a mock function with given fields: ctx, ns, approval, waitConfirm @@ -476,8 +520,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/batchpinmocks/submitter.go b/mocks/batchpinmocks/submitter.go index 2902e504e2..612c3ef257 100644 --- a/mocks/batchpinmocks/submitter.go +++ b/mocks/batchpinmocks/submitter.go @@ -14,6 +14,64 @@ 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) + + 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 e47d84e439..7c1e22e30b 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) @@ -193,6 +207,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 9ff2279f39..0c3cc1c869 100644 --- a/mocks/contractmocks/manager.go +++ b/mocks/contractmocks/manager.go @@ -380,6 +380,64 @@ 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) + + 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 +} + // ValidateFFIAndSetPathnames provides a mock function with given fields: ctx, ffi func (_m *Manager) ValidateFFIAndSetPathnames(ctx context.Context, ffi *fftypes.FFI) error { ret := _m.Called(ctx, ffi) diff --git a/mocks/databasemocks/plugin.go b/mocks/databasemocks/plugin.go index c92af2989e..0685d7f639 100644 --- a/mocks/databasemocks/plugin.go +++ b/mocks/databasemocks/plugin.go @@ -2473,6 +2473,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 +} + // UpdatePins provides a mock function with given fields: ctx, filter, update func (_m *Plugin) UpdatePins(ctx context.Context, filter database.Filter, update database.Update) error { ret := _m.Called(ctx, filter, update) diff --git a/mocks/operationmocks/manager.go b/mocks/operationmocks/manager.go new file mode 100644 index 0000000000..21de2f5bfe --- /dev/null +++ b/mocks/operationmocks/manager.go @@ -0,0 +1,96 @@ +// 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" + + operations "github.com/hyperledger/firefly/internal/operations" +) + +// Manager is an autogenerated mock type for the Manager type +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) + + 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: 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 +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) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.PreparedOperation) error); ok { + r0 = rf(ctx, op) + } else { + r0 = ret.Error(0) + } + + return r0 +} 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/privatemessagingmocks/manager.go b/mocks/privatemessagingmocks/manager.go index be51e56a61..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) @@ -110,6 +124,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 +193,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) diff --git a/mocks/tokenmocks/plugin.go b/mocks/tokenmocks/plugin.go index 565ce03000..671f621062 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/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 45ba9706d9..c5e2b454ec 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) @@ -764,6 +767,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 af611aa11d..7ed87af556 100644 --- a/pkg/fftypes/operation.go +++ b/pkg/fftypes/operation.go @@ -48,7 +48,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" @@ -86,4 +86,15 @@ 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 +// 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"` } diff --git a/pkg/tokens/plugin.go b/pkg/tokens/plugin.go index 112204750f..9c3464a6f9 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