diff --git a/Makefile b/Makefile index 768ed9af95..d66ed469aa 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,7 @@ $(eval $(call makemock, pkg/dataexchange, Callbacks, dataexcha $(eval $(call makemock, pkg/tokens, Plugin, tokenmocks)) $(eval $(call makemock, pkg/tokens, Callbacks, tokenmocks)) $(eval $(call makemock, pkg/wsclient, WSClient, wsmocks)) +$(eval $(call makemock, internal/txcommon, Helper, txcommonmocks)) $(eval $(call makemock, internal/identity, Manager, identitymanagermocks)) $(eval $(call makemock, internal/batchpin, Submitter, batchpinmocks)) $(eval $(call makemock, internal/sysmessaging, SystemEvents, sysmessagingmocks)) diff --git a/db/migrations/postgres/000056_refactor_transactions_columns.down.sql b/db/migrations/postgres/000056_refactor_transactions_columns.down.sql index fa6aa34f01..dd4d8c16e9 100644 --- a/db/migrations/postgres/000056_refactor_transactions_columns.down.sql +++ b/db/migrations/postgres/000056_refactor_transactions_columns.down.sql @@ -4,6 +4,7 @@ ALTER TABLE transactions ADD COLUMN signer VARCHAR(1024); ALTER TABLE transactions ADD COLUMN hash CHAR(64); ALTER TABLE transactions ADD COLUMN protocol_id VARCHAR(256); ALTER TABLE transactions ADD COLUMN info BYTEA; +ALTER TABLE transactions ADD COLUMN status VARCHAR(64); CREATE INDEX transactions_protocol_id ON transactions(protocol_id); CREATE INDEX transactions_ref ON transactions(ref); diff --git a/db/migrations/postgres/000056_refactor_transactions_columns.up.sql b/db/migrations/postgres/000056_refactor_transactions_columns.up.sql index 6b04cf7ec8..75c043ab36 100644 --- a/db/migrations/postgres/000056_refactor_transactions_columns.up.sql +++ b/db/migrations/postgres/000056_refactor_transactions_columns.up.sql @@ -7,6 +7,7 @@ ALTER TABLE transactions DROP COLUMN signer; ALTER TABLE transactions DROP COLUMN hash; ALTER TABLE transactions DROP COLUMN protocol_id; ALTER TABLE transactions DROP COLUMN info; +ALTER TABLE transactions DROP COLUMN status; ALTER TABLE transactions ADD COLUMN blockchain_ids VARCHAR(1024); CREATE INDEX transactions_blockchain_ids ON transactions(blockchain_ids); diff --git a/db/migrations/sqlite/000056_refactor_transactions_columns.down.sql b/db/migrations/sqlite/000056_refactor_transactions_columns.down.sql index b795e4c4e5..14c300a1bb 100644 --- a/db/migrations/sqlite/000056_refactor_transactions_columns.down.sql +++ b/db/migrations/sqlite/000056_refactor_transactions_columns.down.sql @@ -3,6 +3,7 @@ ALTER TABLE transactions ADD COLUMN signer VARCHAR(1024); ALTER TABLE transactions ADD COLUMN hash CHAR(64); ALTER TABLE transactions ADD COLUMN protocol_id VARCHAR(256); ALTER TABLE transactions ADD COLUMN info BYTEA; +ALTER TABLE transactions ADD COLUMN status VARCHAR(64); CREATE INDEX transactions_protocol_id ON transactions(protocol_id); CREATE INDEX transactions_ref ON transactions(ref); diff --git a/db/migrations/sqlite/000056_refactor_transactions_columns.up.sql b/db/migrations/sqlite/000056_refactor_transactions_columns.up.sql index 57d2643991..0d57e9bc6a 100644 --- a/db/migrations/sqlite/000056_refactor_transactions_columns.up.sql +++ b/db/migrations/sqlite/000056_refactor_transactions_columns.up.sql @@ -6,6 +6,7 @@ ALTER TABLE transactions DROP COLUMN signer; ALTER TABLE transactions DROP COLUMN hash; ALTER TABLE transactions DROP COLUMN protocol_id; ALTER TABLE transactions DROP COLUMN info; +ALTER TABLE transactions DROP COLUMN status; ALTER TABLE transactions ADD COLUMN blockchain_ids VARCHAR(1024); CREATE INDEX transactions_blockchain_ids ON transactions(blockchain_ids); diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 5fcc64e0e2..e7754e84c4 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -4231,6 +4231,7 @@ paths: type: integer type: enum: + - transaction_submitted - message_confirmed - message_rejected - namespace_confirmed @@ -4291,6 +4292,7 @@ paths: type: integer type: enum: + - transaction_submitted - message_confirmed - message_rejected - namespace_confirmed @@ -4962,6 +4964,7 @@ paths: type: integer type: enum: + - transaction_submitted - message_confirmed - message_rejected - namespace_confirmed @@ -5086,8 +5089,6 @@ paths: id: {} namespace: type: string - status: - type: string type: enum: - none @@ -6878,6 +6879,16 @@ paths: name: symbol schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: tx.id + schema: + type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: tx.type + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: type @@ -9048,6 +9059,16 @@ paths: name: symbol schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: tx.id + schema: + type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: tx.type + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: type @@ -9861,11 +9882,6 @@ paths: name: namespace schema: type: string - - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' - in: query - name: status - schema: - type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: type @@ -9919,8 +9935,6 @@ paths: id: {} namespace: type: string - status: - type: string type: enum: - none @@ -9978,11 +9992,6 @@ paths: name: namespace schema: type: string - - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' - in: query - name: status - schema: - type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: type @@ -10036,8 +10045,6 @@ paths: id: {} namespace: type: string - status: - type: string type: enum: - none @@ -10181,6 +10188,61 @@ paths: description: Success default: description: "" + /namespaces/{ns}/transactions/{txnid}/status: + get: + description: 'TODO: Description' + operationId: getTxnStatus + parameters: + - description: 'TODO: Description' + in: path + name: ns + required: true + schema: + example: default + type: string + - description: 'TODO: Description' + in: path + name: txnid + 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 + responses: + "200": + content: + application/json: + schema: + properties: + details: + items: + properties: + error: + type: string + id: {} + info: + additionalProperties: {} + type: object + status: + type: string + subtype: + type: string + timestamp: {} + type: + type: string + type: object + type: array + status: + type: string + type: object + description: Success + default: + description: "" /network/nodes: get: description: 'TODO: Description' diff --git a/internal/apiserver/route_get_txn_status.go b/internal/apiserver/route_get_txn_status.go new file mode 100644 index 0000000000..84f4c04776 --- /dev/null +++ b/internal/apiserver/route_get_txn_status.go @@ -0,0 +1,45 @@ +// 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 ( + "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 getTxnStatus = &oapispec.Route{ + Name: "getTxnStatus", + Path: "namespaces/{ns}/transactions/{txnid}/status", + Method: http.MethodGet, + PathParams: []*oapispec.PathParam{ + {Name: "ns", ExampleFromConf: config.NamespacesDefault, Description: i18n.MsgTBD}, + {Name: "txnid", Description: i18n.MsgTBD}, + }, + QueryParams: nil, + FilterFactory: nil, + Description: i18n.MsgTBD, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &fftypes.TransactionStatus{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { + return getOr(r.Ctx).GetTransactionStatus(r.Ctx, r.PP["ns"], r.PP["txnid"]) + }, +} diff --git a/internal/apiserver/route_get_txn_status_test.go b/internal/apiserver/route_get_txn_status_test.go new file mode 100644 index 0000000000..e64ea05d48 --- /dev/null +++ b/internal/apiserver/route_get_txn_status_test.go @@ -0,0 +1,39 @@ +// 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 ( + "net/http/httptest" + "testing" + + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetTxnStatus(t *testing.T) { + o, r := newTestAPIServer() + req := httptest.NewRequest("GET", "/api/v1/namespaces/mynamespace/transactions/abcd12345/status", nil) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + o.On("GetTransactionStatus", mock.Anything, "mynamespace", "abcd12345"). + Return(&fftypes.TransactionStatus{}, nil, nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 200, res.Result().StatusCode) +} diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index f17e7b4720..2cdd90eaee 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -78,6 +78,7 @@ var routes = []*oapispec.Route{ getTxnByID, getTxnOps, getTxnBlockchainEvents, + getTxnStatus, getTxns, getChartHistogram, diff --git a/internal/assets/manager.go b/internal/assets/manager.go index b692dd734a..d516756f43 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -28,6 +28,7 @@ import ( "github.com/hyperledger/firefly/internal/retry" "github.com/hyperledger/firefly/internal/syncasync" "github.com/hyperledger/firefly/internal/sysmessaging" + "github.com/hyperledger/firefly/internal/txcommon" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/hyperledger/firefly/pkg/tokens" @@ -70,6 +71,7 @@ type Manager interface { type assetManager struct { ctx context.Context database database.Plugin + txHelper txcommon.Helper identity identity.Manager data data.Manager syncasync syncasync.Bridge @@ -86,6 +88,7 @@ func NewAssetManager(ctx context.Context, di database.Plugin, im identity.Manage am := &assetManager{ ctx: ctx, database: di, + txHelper: txcommon.NewTransactionHelper(di), identity: im, data: dm, syncasync: sa, diff --git a/internal/assets/manager_test.go b/internal/assets/manager_test.go index 2dde3dc10f..45b3d0b8f9 100644 --- a/internal/assets/manager_test.go +++ b/internal/assets/manager_test.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly/mocks/privatemessagingmocks" "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" "github.com/hyperledger/firefly/pkg/tokens" @@ -51,7 +52,9 @@ func newTestAssets(t *testing.T) (*assetManager, func()) { rag.ReturnArguments = mock.Arguments{a[1].(func(context.Context) error)(a[0].(context.Context))} } assert.NoError(t, err) - return a.(*assetManager), cancel + am := a.(*assetManager) + am.txHelper = &txcommonmocks.Helper{} + return am, cancel } func TestInitFail(t *testing.T) { diff --git a/internal/assets/token_pool.go b/internal/assets/token_pool.go index ceb4d8ec26..338d3b829c 100644 --- a/internal/assets/token_pool.go +++ b/internal/assets/token_pool.go @@ -84,31 +84,26 @@ func (am *assetManager) createTokenPoolInternal(ctx context.Context, pool *fftyp }) } - tx := &fftypes.Transaction{ - ID: fftypes.NewUUID(), - Namespace: pool.Namespace, - Type: fftypes.TransactionTypeTokenPool, - Created: fftypes.Now(), - Status: fftypes.OpStatusPending, - } - pool.TX.ID = tx.ID - pool.TX.Type = tx.Type - - op := fftypes.NewTXOperation( - plugin, - pool.Namespace, - tx.ID, - "", - fftypes.OpTypeTokenCreatePool, - fftypes.OpStatusPending) - txcommon.AddTokenPoolCreateInputs(op, pool) - + var op *fftypes.Operation err = am.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { - err = am.database.UpsertTransaction(ctx, tx) - if err == nil { - err = am.database.InsertOperation(ctx, op) + txid, err := am.txHelper.SubmitNewTransaction(ctx, pool.Namespace, fftypes.TransactionTypeTokenPool) + if err != nil { + return err } - return err + + pool.TX.ID = txid + pool.TX.Type = fftypes.TransactionTypeTokenPool + + op = fftypes.NewTXOperation( + plugin, + pool.Namespace, + txid, + "", + fftypes.OpTypeTokenCreatePool, + fftypes.OpStatusPending) + txcommon.AddTokenPoolCreateInputs(op, pool) + + return am.database.InsertOperation(ctx, op) }) if err != nil { return nil, err diff --git a/internal/assets/token_pool_test.go b/internal/assets/token_pool_test.go index bbff4ef2e6..a6dd51b089 100644 --- a/internal/assets/token_pool_test.go +++ b/internal/assets/token_pool_test.go @@ -25,6 +25,7 @@ import ( "github.com/hyperledger/firefly/mocks/identitymanagermocks" "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" "github.com/hyperledger/firefly/pkg/tokens" @@ -89,11 +90,10 @@ func TestCreateTokenPoolFail(t *testing.T) { mdm := am.data.(*datamocks.Manager) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).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(fmt.Errorf("pop")) @@ -109,12 +109,12 @@ func TestCreateTokenPoolTransactionFail(t *testing.T) { Name: "testpool", } - mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.Anything).Return(fmt.Errorf("pop")) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(nil, fmt.Errorf("pop")) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) assert.Regexp(t, "pop", err) @@ -131,11 +131,10 @@ func TestCreateTokenPoolOperationFail(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(fmt.Errorf("pop")) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) @@ -154,12 +153,11 @@ func TestCreateTokenPoolSuccess(t *testing.T) { mdm := am.data.(*datamocks.Manager) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) @@ -178,12 +176,11 @@ func TestCreateTokenPoolUnknownConnectorSuccess(t *testing.T) { mdm := am.data.(*datamocks.Manager) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.CreateTokenPool(context.Background(), "ns1", pool, false) @@ -238,12 +235,11 @@ func TestCreateTokenPoolConfirm(t *testing.T) { msa := am.syncasync.(*syncasyncmocks.Bridge) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil).Times(2) mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything).Return(nil).Times(1) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil).Times(1) msa.On("WaitForTokenPool", context.Background(), "ns1", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { @@ -317,11 +313,10 @@ func TestCreateTokenPoolByTypeFail(t *testing.T) { mdm := am.data.(*datamocks.Manager) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).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(fmt.Errorf("pop")) @@ -337,12 +332,12 @@ func TestCreateTokenPoolByTypeTransactionFail(t *testing.T) { Name: "testpool", } - mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.Anything).Return(fmt.Errorf("pop")) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(nil, fmt.Errorf("pop")) _, err := am.CreateTokenPoolByType(context.Background(), "ns1", "magic-tokens", pool, false) assert.Regexp(t, "pop", err) @@ -359,11 +354,10 @@ func TestCreateTokenPoolByTypeOperationFail(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mdm := am.data.(*datamocks.Manager) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(fmt.Errorf("pop")) _, err := am.CreateTokenPoolByType(context.Background(), "ns1", "magic-tokens", pool, false) @@ -382,12 +376,11 @@ func TestCreateTokenPoolByTypeSuccess(t *testing.T) { mdm := am.data.(*datamocks.Manager) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.CreateTokenPoolByType(context.Background(), "ns1", "magic-tokens", pool, false) @@ -407,12 +400,11 @@ func TestCreateTokenPoolByTypeConfirm(t *testing.T) { msa := am.syncasync.(*syncasyncmocks.Bridge) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil).Times(2) mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything).Return(nil).Times(1) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenPool).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil).Times(1) msa.On("WaitForTokenPool", context.Background(), "ns1", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go index ca3fbebd20..60f94b629a 100644 --- a/internal/assets/token_transfer.go +++ b/internal/assets/token_transfer.go @@ -246,26 +246,8 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er return nil } - tx := &fftypes.Transaction{ - ID: fftypes.NewUUID(), - Namespace: s.namespace, - Type: fftypes.TransactionTypeTokenTransfer, - Created: fftypes.Now(), - Status: fftypes.OpStatusPending, - } - s.transfer.TX.ID = tx.ID - s.transfer.TX.Type = tx.Type - - op := fftypes.NewTXOperation( - plugin, - s.namespace, - tx.ID, - "", - fftypes.OpTypeTokenTransfer, - fftypes.OpStatusPending) - txcommon.AddTokenTransferInputs(op, &s.transfer.TokenTransfer) - var pool *fftypes.TokenPool + var op *fftypes.Operation err = s.mgr.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { pool, err = s.mgr.GetTokenPoolByNameOrID(ctx, s.namespace, s.transfer.Pool) if err != nil { @@ -275,10 +257,23 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er return i18n.NewError(ctx, i18n.MsgTokenPoolNotConfirmed) } - err = s.mgr.database.UpsertTransaction(ctx, tx) + txid, err := s.mgr.txHelper.SubmitNewTransaction(ctx, s.namespace, fftypes.TransactionTypeTokenTransfer) if err != nil { return err } + + s.transfer.TX.ID = txid + s.transfer.TX.Type = fftypes.TransactionTypeTokenTransfer + + op = fftypes.NewTXOperation( + plugin, + s.namespace, + txid, + "", + fftypes.OpTypeTokenTransfer, + fftypes.OpStatusPending) + txcommon.AddTokenTransferInputs(op, &s.transfer.TokenTransfer) + if err = s.mgr.database.InsertOperation(ctx, op); err != nil { return err } @@ -303,17 +298,12 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er panic(fmt.Sprintf("unknown transfer type: %v", s.transfer.Type)) } - // if transaction fails, mark tx and op as failed in DB + // if transaction fails, mark op as failed in DB if err != nil { _ = s.mgr.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { l := log.L(ctx) - tx.Status = fftypes.OpStatusFailed - update := database.OperationQueryFactory.NewUpdate(ctx). Set("status", fftypes.OpStatusFailed) - if err = s.mgr.database.UpdateTransaction(ctx, tx.ID, update); err != nil { - l.Errorf("TX update failed: %s update=[ %s ]", err, update) - } if err = s.mgr.database.UpdateOperation(ctx, op.ID, update); err != nil { l.Errorf("Operation update failed: %s update=[ %s ]", err, update) } diff --git a/internal/assets/token_transfer_test.go b/internal/assets/token_transfer_test.go index 27ef4deaa9..38fc216a2e 100644 --- a/internal/assets/token_transfer_test.go +++ b/internal/assets/token_transfer_test.go @@ -28,6 +28,7 @@ import ( "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" "github.com/hyperledger/firefly/pkg/tokens" @@ -124,12 +125,11 @@ func TestMintTokensSuccess(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -154,12 +154,11 @@ func TestMintTokenUnknownConnectorSuccess(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -257,6 +256,7 @@ 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) fb := database.TokenPoolQueryFactory.NewFilter(context.Background()) f := fb.And() f.Limit(1).Count(true) @@ -278,9 +278,7 @@ func TestMintTokenUnknownPoolSuccess(t *testing.T) { }))).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) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -447,14 +445,12 @@ func TestMintTokensFail(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(fmt.Errorf("pop")) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mdi.On("UpdateTransaction", context.Background(), mock.Anything, mock.Anything).Return(nil) mdi.On("UpdateOperation", context.Background(), mock.Anything, mock.Anything).Return(nil) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -479,14 +475,12 @@ func TestMintTokensFailAndDbFail(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(fmt.Errorf("pop")) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer && tx.Status != fftypes.OpStatusFailed - })).Return(nil) - mdi.On("UpdateTransaction", context.Background(), mock.Anything, mock.Anything).Return(fmt.Errorf("Update fail")) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("UpdateOperation", context.Background(), mock.Anything, mock.Anything).Return(fmt.Errorf("Update fail")) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -510,11 +504,10 @@ func TestMintTokensOperationFail(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(fmt.Errorf("pop")) _, err := am.MintTokens(context.Background(), "ns1", mint, false) @@ -541,12 +534,11 @@ func TestMintTokensConfirm(t *testing.T) { msa := am.syncasync.(*syncasyncmocks.Bridge) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).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). Run(func(args mock.Arguments) { @@ -581,12 +573,11 @@ func TestMintTokensByTypeSuccess(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("MintTokens", context.Background(), mock.Anything, "F1", &mint.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.MintTokensByType(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) @@ -611,12 +602,11 @@ func TestBurnTokensSuccess(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("BurnTokens", context.Background(), mock.Anything, "F1", &burn.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.BurnTokens(context.Background(), "ns1", burn, false) @@ -665,12 +655,11 @@ func TestBurnTokensConfirm(t *testing.T) { msa := am.syncasync.(*syncasyncmocks.Bridge) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("BurnTokens", context.Background(), mock.Anything, "F1", &burn.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).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). Run(func(args mock.Arguments) { @@ -705,12 +694,11 @@ func TestBurnTokensByTypeSuccess(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("BurnTokens", context.Background(), mock.Anything, "F1", &burn.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.BurnTokensByType(context.Background(), "ns1", "magic-tokens", "pool1", burn, false) @@ -741,12 +729,11 @@ func TestTransferTokensSuccess(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) @@ -842,10 +829,9 @@ func TestTransferTokensInvalidType(t *testing.T) { } mdi := am.database.(*databasemocks.Plugin) + mth := am.txHelper.(*txcommonmocks.Helper) mdi.On("GetTokenPool", am.ctx, "ns1", "pool1").Return(pool, nil) - mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", am.ctx, "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", am.ctx, mock.Anything).Return(nil) sender := &transferSender{ @@ -877,11 +863,10 @@ func TestTransferTokensTransactionFail(t *testing.T) { mdi := am.database.(*databasemocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(fmt.Errorf("pop")) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(nil, fmt.Errorf("pop")) _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) assert.EqualError(t, err, "pop") @@ -927,12 +912,11 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { mim := am.identity.(*identitymanagermocks.Manager) mbm := am.broadcast.(*broadcastmocks.Manager) mms := &sysmessagingmocks.MessageSender{} + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).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) mms.On("Prepare", context.Background()).Return(nil) @@ -1025,12 +1009,11 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { mim := am.identity.(*identitymanagermocks.Manager) mpm := am.messaging.(*privatemessagingmocks.Manager) mms := &sysmessagingmocks.MessageSender{} + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).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) mms.On("Prepare", context.Background()).Return(nil) @@ -1106,12 +1089,11 @@ func TestTransferTokensConfirm(t *testing.T) { msa := am.syncasync.(*syncasyncmocks.Bridge) mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) mim := am.identity.(*identitymanagermocks.Manager) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).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). Run(func(args mock.Arguments) { @@ -1167,13 +1149,12 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { mbm := am.broadcast.(*broadcastmocks.Manager) mms := &sysmessagingmocks.MessageSender{} msa := am.syncasync.(*syncasyncmocks.Bridge) + mth := am.txHelper.(*txcommonmocks.Helper) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) mms.On("Prepare", context.Background()).Return(nil) mdi.On("UpsertMessage", context.Background(), mock.MatchedBy(func(msg *fftypes.Message) bool { @@ -1224,12 +1205,11 @@ func TestTransferTokensByTypeSuccess(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) mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) mti.On("TransferTokens", context.Background(), mock.Anything, "F1", &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("SubmitNewTransaction", context.Background(), "ns1", fftypes.TransactionTypeTokenTransfer).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", context.Background(), mock.Anything).Return(nil) _, err := am.TransferTokensByType(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) diff --git a/internal/batch/batch_manager_test.go b/internal/batch/batch_manager_test.go index f98195b2d9..09549721ba 100644 --- a/internal/batch/batch_manager_test.go +++ b/internal/batch/batch_manager_test.go @@ -118,6 +118,8 @@ func TestE2EDispatchBroadcast(t *testing.T) { assert.Equal(t, fmt.Sprintf("id IN ['%s']", msg.Header.ID.String()), fi.String()) return true }), mock.Anything).Return(nil) + mdi.On("InsertTransaction", mock.Anything, mock.Anything).Return(nil) + mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil) // transaction submit err := bm.Start() assert.NoError(t, err) @@ -231,6 +233,8 @@ func TestE2EDispatchPrivate(t *testing.T) { a[1].(*fftypes.Nonce).Nonce = nextNonce nextNonce++ } + mdi.On("InsertTransaction", mock.Anything, mock.Anything).Return(nil) + mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil) // transaction submit err := bm.Start() assert.NoError(t, err) @@ -350,6 +354,10 @@ func TestMessageSequencerMissingMessageData(t *testing.T) { mdm.On("GetMessageData", mock.Anything, mock.Anything, true).Return(nil, false, nil) bm.(*batchManager).messageSequencer() + + bm.Close() + bm.WaitStop() + mdi.AssertExpectations(t) mdm.AssertExpectations(t) } @@ -378,6 +386,10 @@ func TestMessageSequencerDispatchFail(t *testing.T) { mdm.On("GetMessageData", mock.Anything, mock.Anything, true).Return([]*fftypes.Data{{ID: dataID}}, true, nil) bm.(*batchManager).messageSequencer() + + bm.Close() + bm.WaitStop() + mdi.AssertExpectations(t) mdm.AssertExpectations(t) } @@ -420,6 +432,10 @@ func TestMessageSequencerUpdateMessagesFail(t *testing.T) { } bm.(*batchManager).messageSequencer() + + bm.Close() + bm.WaitStop() + mdi.AssertExpectations(t) mdm.AssertExpectations(t) } @@ -461,8 +477,14 @@ func TestMessageSequencerUpdateBatchFail(t *testing.T) { } rag.ReturnArguments = mock.Arguments{err} } + mdi.On("InsertTransaction", mock.Anything, mock.Anything).Return(nil).Maybe() + mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil).Maybe() bm.(*batchManager).messageSequencer() + + bm.Close() + bm.WaitStop() + mdi.AssertExpectations(t) mdm.AssertExpectations(t) } diff --git a/internal/batch/batch_processor.go b/internal/batch/batch_processor.go index 54d6f738dd..5fa2c62235 100644 --- a/internal/batch/batch_processor.go +++ b/internal/batch/batch_processor.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly/internal/log" "github.com/hyperledger/firefly/internal/retry" "github.com/hyperledger/firefly/internal/sysmessaging" + "github.com/hyperledger/firefly/internal/txcommon" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" ) @@ -56,6 +57,7 @@ type batchProcessor struct { ctx context.Context ni sysmessaging.LocalNodeInfo database database.Plugin + txHelper txcommon.Helper name string cancelCtx func() closed bool @@ -77,6 +79,7 @@ func newBatchProcessor(ctx context.Context, ni sysmessaging.LocalNodeInfo, di da cancelCtx: cancelCtx, ni: ni, database: di, + txHelper: txcommon.NewTransactionHelper(di), name: fmt.Sprintf("%s:%s:%s", conf.namespace, conf.identity.Author, conf.identity.Key), newWork: make(chan *batchWork), persistWork: make(chan *batchWork, conf.BatchMaxSize), @@ -295,15 +298,16 @@ func (bp *batchProcessor) persistBatch(batch *fftypes.Batch, newWork []*batchWor Set("group", batch.Group) err = bp.database.UpdateMessages(ctx, filter, update) } + if err == nil && seal { - // Generate a new Transaction reference, which will be used to record status of the associated transaction as it happens - batch.Payload.TX = fftypes.TransactionRef{ - Type: fftypes.TransactionTypeBatchPin, - ID: fftypes.NewUUID(), - } + // Generate a new Transaction, which will be used to record status of the associated transaction as it happens contexts, err = bp.maskContexts(ctx, batch) - batch.Hash = batch.Payload.Hash() - log.L(ctx).Debugf("Batch %s sealed. Hash=%s", batch.ID, batch.Hash) + if err == nil { + batch.Payload.TX.Type = fftypes.TransactionTypeBatchPin + batch.Payload.TX.ID, err = bp.txHelper.SubmitNewTransaction(ctx, batch.Namespace, fftypes.TransactionTypeBatchPin) + batch.Hash = batch.Payload.Hash() + log.L(ctx).Debugf("Batch %s sealed. Hash=%s", batch.ID, batch.Hash) + } } if err == nil { // Persist the batch itself diff --git a/internal/batch/batch_processor_test.go b/internal/batch/batch_processor_test.go index 8d62b160d6..3c0ec19765 100644 --- a/internal/batch/batch_processor_test.go +++ b/internal/batch/batch_processor_test.go @@ -25,6 +25,7 @@ import ( "github.com/hyperledger/firefly/internal/retry" "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/sysmessagingmocks" + "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -49,6 +50,7 @@ func newTestBatchProcessor(dispatch DispatchHandler) (*databasemocks.Plugin, *ba InitialDelay: 1 * time.Microsecond, MaximumDelay: 1 * time.Microsecond, }) + bp.txHelper = &txcommonmocks.Helper{} return mdi, bp } @@ -77,6 +79,9 @@ func TestUnfilledBatch(t *testing.T) { mdi.On("UpsertBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) mdi.On("UpdateBatch", mock.Anything, mock.Anything).Return(nil) + mth := bp.txHelper.(*txcommonmocks.Helper) + mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeBatchPin).Return(fftypes.NewUUID(), nil) + // Generate the work work := make([]*batchWork, 5) for i := 0; i < len(work); i++ { @@ -129,6 +134,9 @@ func TestBatchSizeOverflow(t *testing.T) { mdi.On("UpsertBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) mdi.On("UpdateBatch", mock.Anything, mock.Anything).Return(nil) + mth := bp.txHelper.(*txcommonmocks.Helper) + mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeBatchPin).Return(fftypes.NewUUID(), nil) + // Generate the work work := make([]*batchWork, 2) for i := 0; i < 2; i++ { @@ -185,6 +193,9 @@ func TestFilledBatchSlowPersistence(t *testing.T) { mdi.On("UpdateMessages", mock.Anything, mock.Anything, mock.Anything).Return(nil) mdi.On("UpdateBatch", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mth := bp.txHelper.(*txcommonmocks.Helper) + mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeBatchPin).Return(fftypes.NewUUID(), nil) + // Generate the work work := make([]*batchWork, 10) for i := 0; i < 10; i++ { diff --git a/internal/batchpin/batchpin.go b/internal/batchpin/batchpin.go index 9e74cda99d..38fea250fa 100644 --- a/internal/batchpin/batchpin.go +++ b/internal/batchpin/batchpin.go @@ -48,17 +48,6 @@ func NewBatchPinSubmitter(di database.Plugin, im identity.Manager, bi blockchain } func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *fftypes.Batch, contexts []*fftypes.Bytes32) error { - tx := &fftypes.Transaction{ - ID: batch.Payload.TX.ID, - Type: fftypes.TransactionTypeBatchPin, - Namespace: batch.Namespace, - Created: fftypes.Now(), - Status: fftypes.OpStatusPending, - } - err := bp.database.UpsertTransaction(ctx, tx) - if err != nil { - return err - } // The pending blockchain transaction op := fftypes.NewTXOperation( @@ -68,8 +57,7 @@ func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *fftyp "", fftypes.OpTypeBlockchainBatchPin, fftypes.OpStatusPending) - err = bp.database.InsertOperation(ctx, op) - if err != nil { + if err := bp.database.InsertOperation(ctx, op); err != nil { return err } diff --git a/internal/batchpin/batchpin_test.go b/internal/batchpin/batchpin_test.go index 712238dee5..437687d876 100644 --- a/internal/batchpin/batchpin_test.go +++ b/internal/batchpin/batchpin_test.go @@ -38,7 +38,8 @@ func newTestBatchPinSubmitter(t *testing.T) *batchPinSubmitter { mim := &identitymanagermocks.Manager{} mbi := &blockchainmocks.Plugin{} mbi.On("Name").Return("ut").Maybe() - return NewBatchPinSubmitter(mdi, mim, mbi).(*batchPinSubmitter) + bps := NewBatchPinSubmitter(mdi, mim, mbi).(*batchPinSubmitter) + return bps } func TestSubmitPinnedBatchOk(t *testing.T) { @@ -62,7 +63,6 @@ func TestSubmitPinnedBatchOk(t *testing.T) { } contexts := []*fftypes.Bytes32{} - mdi.On("UpsertTransaction", ctx, mock.Anything).Return(nil) mdi.On("InsertOperation", ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { assert.Equal(t, fftypes.OpTypeBlockchainBatchPin, op.Type) assert.Equal(t, "ut", op.Plugin) @@ -98,7 +98,6 @@ func TestSubmitPinnedBatchWithMetricsOk(t *testing.T) { } contexts := []*fftypes.Bytes32{} - mdi.On("UpsertTransaction", ctx, mock.Anything).Return(nil) mdi.On("InsertOperation", ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { assert.Equal(t, fftypes.OpTypeBlockchainBatchPin, op.Type) assert.Equal(t, "ut", op.Plugin) @@ -132,38 +131,9 @@ func TestSubmitPinnedBatchOpFail(t *testing.T) { } contexts := []*fftypes.Bytes32{} - mdi.On("UpsertTransaction", ctx, mock.Anything).Return(nil) mdi.On("InsertOperation", ctx, mock.Anything).Return(fmt.Errorf("pop")) err := bp.SubmitPinnedBatch(ctx, batch, contexts) assert.Regexp(t, "pop", err) } - -func TestSubmitPinnedBatchTxInsertFail(t *testing.T) { - - bp := newTestBatchPinSubmitter(t) - ctx := context.Background() - - mdi := bp.database.(*databasemocks.Plugin) - - batch := &fftypes.Batch{ - ID: fftypes.NewUUID(), - Identity: fftypes.Identity{ - Author: "id1", - Key: "0x12345", - }, - Payload: fftypes.BatchPayload{ - TX: fftypes.TransactionRef{ - ID: fftypes.NewUUID(), - }, - }, - } - contexts := []*fftypes.Bytes32{} - - mdi.On("UpsertTransaction", ctx, mock.Anything).Return(fmt.Errorf("pop")) - - err := bp.SubmitPinnedBatch(ctx, batch, contexts) - assert.Regexp(t, "pop", err) - -} diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index 2432ae7678..029e688560 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/txcommon" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" @@ -56,6 +57,7 @@ type Manager interface { type contractManager struct { database database.Plugin + txHelper txcommon.Helper publicStorage publicstorage.Plugin broadcast broadcast.Manager identity identity.Manager @@ -73,6 +75,7 @@ func NewContractManager(ctx context.Context, database database.Plugin, publicSto } return &contractManager{ database: database, + txHelper: txcommon.NewTransactionHelper(database), publicStorage: publicStorage, broadcast: broadcast, identity: identity, @@ -175,21 +178,15 @@ func (cm *contractManager) InvokeContract(ctx context.Context, ns string, req *f return err } - tx := &fftypes.Transaction{ - ID: fftypes.NewUUID(), - Namespace: ns, - Type: fftypes.TransactionTypeContractInvoke, - Created: fftypes.Now(), - Status: fftypes.OpStatusPending, - } - if err := cm.database.UpsertTransaction(ctx, tx); err != nil { + txid, err := cm.txHelper.SubmitNewTransaction(ctx, ns, fftypes.TransactionTypeContractInvoke) + if err != nil { return err } op = fftypes.NewTXOperation( cm.blockchain, ns, - tx.ID, + txid, "", fftypes.OpTypeContractInvoke, fftypes.OpStatusPending) diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index c88aecd5b0..946317e16b 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/mocks/publicstoragemocks" + "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/santhosh-tekuri/jsonschema/v5" @@ -51,6 +52,7 @@ func newTestContractManager() *contractManager { } } cm, _ := NewContractManager(context.Background(), mdb, mps, mbm, mim, mbi) + cm.(*contractManager).txHelper = &txcommonmocks.Helper{} return cm.(*contractManager) } @@ -973,6 +975,7 @@ func TestInvokeContract(t *testing.T) { mbi := cm.blockchain.(*blockchainmocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) mdi := cm.database.(*databasemocks.Plugin) + mth := cm.txHelper.(*txcommonmocks.Helper) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeInvoke, @@ -987,10 +990,9 @@ func TestInvokeContract(t *testing.T) { }, } + mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(fftypes.NewUUID(), nil) + mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) - mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke - })).Return(nil) mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Namespace == "ns1" && op.Type == fftypes.OpTypeContractInvoke && op.Plugin == "mockblockchain" })).Return(nil) @@ -999,6 +1001,7 @@ func TestInvokeContract(t *testing.T) { _, err := cm.InvokeContract(context.Background(), "ns1", req) assert.NoError(t, err) + mth.AssertExpectations(t) } func TestInvokeContractFailResolveSigningKey(t *testing.T) { @@ -1042,7 +1045,7 @@ func TestInvokeContractFailResolve(t *testing.T) { func TestInvokeContractTXFail(t *testing.T) { cm := newTestContractManager() mim := cm.identity.(*identitymanagermocks.Manager) - mdi := cm.database.(*databasemocks.Plugin) + mth := cm.txHelper.(*txcommonmocks.Helper) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeInvoke, @@ -1058,9 +1061,7 @@ func TestInvokeContractTXFail(t *testing.T) { } mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) - mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke - })).Return(fmt.Errorf("pop")) + mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(nil, fmt.Errorf("pop")) _, err := cm.InvokeContract(context.Background(), "ns1", req) @@ -1151,6 +1152,7 @@ func TestQueryContract(t *testing.T) { mbi := cm.blockchain.(*blockchainmocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) mdi := cm.database.(*databasemocks.Plugin) + mth := cm.txHelper.(*txcommonmocks.Helper) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeQuery, @@ -1166,9 +1168,7 @@ func TestQueryContract(t *testing.T) { } mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) - mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke - })).Return(nil) + mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Namespace == "ns1" && op.Type == fftypes.OpTypeContractInvoke && op.Plugin == "mockblockchain" })).Return(nil) @@ -1183,6 +1183,7 @@ func TestCallContractInvalidType(t *testing.T) { cm := newTestContractManager() mim := cm.identity.(*identitymanagermocks.Manager) mdi := cm.database.(*databasemocks.Plugin) + mth := cm.txHelper.(*txcommonmocks.Helper) req := &fftypes.ContractCallRequest{ Interface: fftypes.NewUUID(), @@ -1197,9 +1198,7 @@ func TestCallContractInvalidType(t *testing.T) { } mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) - mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke - })).Return(nil) + mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Namespace == "ns1" && op.Type == fftypes.OpTypeContractInvoke && op.Plugin == "mockblockchain" })).Return(nil) @@ -1329,6 +1328,7 @@ func TestInvokeContractAPI(t *testing.T) { mim := cm.identity.(*identitymanagermocks.Manager) mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) + mth := cm.txHelper.(*txcommonmocks.Helper) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeInvoke, @@ -1350,9 +1350,7 @@ func TestInvokeContractAPI(t *testing.T) { mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) mdb.On("GetContractAPIByName", mock.Anything, "ns1", "banana").Return(api, nil) mdb.On("GetFFIMethod", mock.Anything, "ns1", mock.Anything, mock.Anything).Return(&fftypes.FFIMethod{Name: "peel"}, nil) - mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke - })).Return(nil) + mth.On("SubmitNewTransaction", mock.Anything, "ns1", fftypes.TransactionTypeContractInvoke).Return(fftypes.NewUUID(), nil) mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { return op.Namespace == "ns1" && op.Type == fftypes.OpTypeContractInvoke && op.Plugin == "mockblockchain" })).Return(nil) diff --git a/internal/database/sqlcommon/tokenpool_sql.go b/internal/database/sqlcommon/tokenpool_sql.go index 7f1465374d..20d6292aca 100644 --- a/internal/database/sqlcommon/tokenpool_sql.go +++ b/internal/database/sqlcommon/tokenpool_sql.go @@ -1,4 +1,4 @@ -// Copyright © 2021 Kaleido, Inc. +// Copyright © 2022 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -45,10 +45,10 @@ var ( "key", } tokenPoolFilterFieldMap = map[string]string{ - "protocolid": "protocol_id", - "message": "message_id", - "transaction.type": "tx_type", - "transaction.id": "tx_id", + "protocolid": "protocol_id", + "message": "message_id", + "tx.type": "tx_type", + "tx.id": "tx_id", } ) diff --git a/internal/database/sqlcommon/transaction_sql.go b/internal/database/sqlcommon/transaction_sql.go index 883cc73e8c..320774c580 100644 --- a/internal/database/sqlcommon/transaction_sql.go +++ b/internal/database/sqlcommon/transaction_sql.go @@ -33,7 +33,6 @@ var ( "ttype", "namespace", "created", - "status", "blockchain_ids", } transactionFilterFieldMap = map[string]string{ @@ -42,65 +41,30 @@ var ( } ) -func (s *SQLCommon) UpsertTransaction(ctx context.Context, transaction *fftypes.Transaction) (err error) { +func (s *SQLCommon) InsertTransaction(ctx context.Context, transaction *fftypes.Transaction) (err error) { ctx, tx, autoCommit, err := s.beginOrUseTx(ctx) if err != nil { return err } defer s.rollbackTx(ctx, tx, autoCommit) - // Do a select within the transaction to determine if the UUID already exists - transactionRows, _, err := s.queryTx(ctx, tx, - sq.Select("blockchain_ids"). - From("transactions"). - Where(sq.Eq{"id": transaction.ID}), - ) - if err != nil { + transaction.Created = fftypes.Now() + if _, err = s.insertTx(ctx, tx, + sq.Insert("transactions"). + Columns(transactionColumns...). + Values( + transaction.ID, + string(transaction.Type), + transaction.Namespace, + transaction.Created, + transaction.BlockchainIDs, + ), + func() { + s.callbacks.UUIDCollectionNSEvent(database.CollectionTransactions, fftypes.ChangeEventTypeCreated, transaction.Namespace, transaction.ID) + }, + ); err != nil { return err } - existing := transactionRows.Next() - - if existing { - var existingBlockchainIDs fftypes.FFStringArray - _ = transactionRows.Scan(&existingBlockchainIDs) - transaction.BlockchainIDs = transaction.BlockchainIDs.MergeLower(existingBlockchainIDs) - transactionRows.Close() - // Update the transaction - if _, err = s.updateTx(ctx, tx, - sq.Update("transactions"). - Set("ttype", string(transaction.Type)). - Set("namespace", transaction.Namespace). - Set("status", transaction.Status). - Set("blockchain_ids", transaction.BlockchainIDs). - Where(sq.Eq{"id": transaction.ID}), - func() { - s.callbacks.UUIDCollectionNSEvent(database.CollectionTransactions, fftypes.ChangeEventTypeUpdated, transaction.Namespace, transaction.ID) - }, - ); err != nil { - return err - } - } else { - transactionRows.Close() - // Insert a transaction - transaction.Created = fftypes.Now() - if _, err = s.insertTx(ctx, tx, - sq.Insert("transactions"). - Columns(transactionColumns...). - Values( - transaction.ID, - string(transaction.Type), - transaction.Namespace, - transaction.Created, - transaction.Status, - transaction.BlockchainIDs, - ), - func() { - s.callbacks.UUIDCollectionNSEvent(database.CollectionTransactions, fftypes.ChangeEventTypeCreated, transaction.Namespace, transaction.ID) - }, - ); err != nil { - return err - } - } return s.commitTx(ctx, tx, autoCommit) } @@ -112,7 +76,6 @@ func (s *SQLCommon) transactionResult(ctx context.Context, row *sql.Rows) (*ffty &transaction.Type, &transaction.Namespace, &transaction.Created, - &transaction.Status, &transaction.BlockchainIDs, ) if err != nil { diff --git a/internal/database/sqlcommon/transaction_sql_test.go b/internal/database/sqlcommon/transaction_sql_test.go index cd829d11c1..01c299f818 100644 --- a/internal/database/sqlcommon/transaction_sql_test.go +++ b/internal/database/sqlcommon/transaction_sql_test.go @@ -41,14 +41,13 @@ func TestTransactionE2EWithDB(t *testing.T) { ID: transactionID, Type: fftypes.TransactionTypeBatchPin, Namespace: "ns1", - Status: fftypes.OpStatusPending, BlockchainIDs: fftypes.FFStringArray{"tx1"}, } s.callbacks.On("UUIDCollectionNSEvent", database.CollectionTransactions, fftypes.ChangeEventTypeCreated, "ns1", transactionID, mock.Anything).Return() s.callbacks.On("UUIDCollectionNSEvent", database.CollectionTransactions, fftypes.ChangeEventTypeUpdated, "ns1", transactionID, mock.Anything).Return() - err := s.UpsertTransaction(ctx, transaction) + err := s.InsertTransaction(ctx, transaction) assert.NoError(t, err) // Check we get the exact same transaction back @@ -59,32 +58,10 @@ func TestTransactionE2EWithDB(t *testing.T) { transactionReadJson, _ := json.Marshal(&transactionRead) assert.Equal(t, string(transactionJson), string(transactionReadJson)) - // Update the transaction - transactionUpdated := &fftypes.Transaction{ - ID: transactionID, - Type: fftypes.TransactionTypeBatchPin, - Namespace: "ns1", - Created: transaction.Created, - Status: fftypes.OpStatusFailed, - BlockchainIDs: fftypes.FFStringArray{"tx2", "tx3"}, // additive - } - err = s.UpsertTransaction(context.Background(), transactionUpdated) - assert.NoError(t, err) - - // Expect merged - transactionUpdated.BlockchainIDs = fftypes.FFStringArray{"tx1", "tx2", "tx3"} - - // Check we get the exact same message back - note the removal of one of the transaction elements - transactionRead, err = s.GetTransactionByID(ctx, transactionID) - assert.NoError(t, err) - transactionJson, _ = json.Marshal(&transactionUpdated) - transactionReadJson, _ = json.Marshal(&transactionRead) - assert.Equal(t, string(transactionJson), string(transactionReadJson)) - // Query back the transaction fb := database.TransactionQueryFactory.NewFilter(ctx) filter := fb.And( - fb.Eq("id", transactionUpdated.ID.String()), + fb.Eq("id", transaction.ID.String()), fb.Gt("created", "0"), ) transactions, res, err := s.GetTransactions(ctx, filter.Count(true)) @@ -96,7 +73,7 @@ func TestTransactionE2EWithDB(t *testing.T) { // Negative test on filter filter = fb.And( - fb.Eq("id", transactionUpdated.ID.String()), + fb.Eq("id", transaction.ID.String()), fb.Eq("created", "0"), ) transactions, _, err = s.GetTransactions(ctx, filter) @@ -105,71 +82,46 @@ func TestTransactionE2EWithDB(t *testing.T) { // Update up := database.TransactionQueryFactory.NewUpdate(ctx). - Set("status", fftypes.OpStatusSucceeded) - err = s.UpdateTransaction(ctx, transactionUpdated.ID, up) + Set("blockchainids", fftypes.FFStringArray{"0x12345", "0x23456"}) + err = s.UpdateTransaction(ctx, transaction.ID, up) assert.NoError(t, err) // Test find updated value filter = fb.And( - fb.Eq("id", transactionUpdated.ID.String()), - fb.Eq("status", fftypes.OpStatusSucceeded), + fb.Eq("id", transaction.ID.String()), + fb.Eq("blockchainids", "0x12345,0x23456"), ) transactions, _, err = s.GetTransactions(ctx, filter) assert.NoError(t, err) assert.Equal(t, 1, len(transactions)) } -func TestUpsertTransactionFailBegin(t *testing.T) { +func TestInsertTransactionFailBegin(t *testing.T) { s, mock := newMockProvider().init() mock.ExpectBegin().WillReturnError(fmt.Errorf("pop")) - err := s.UpsertTransaction(context.Background(), &fftypes.Transaction{}) + err := s.InsertTransaction(context.Background(), &fftypes.Transaction{}) assert.Regexp(t, "FF10114", err) assert.NoError(t, mock.ExpectationsWereMet()) } -func TestUpsertTransactionFailSelect(t *testing.T) { +func TestInsertTransactionFailInsert(t *testing.T) { s, mock := newMockProvider().init() mock.ExpectBegin() - mock.ExpectQuery("SELECT .*").WillReturnError(fmt.Errorf("pop")) - mock.ExpectRollback() - transactionID := fftypes.NewUUID() - err := s.UpsertTransaction(context.Background(), &fftypes.Transaction{ID: transactionID}) - assert.Regexp(t, "FF10115", err) - assert.NoError(t, mock.ExpectationsWereMet()) -} - -func TestUpsertTransactionFailInsert(t *testing.T) { - s, mock := newMockProvider().init() - mock.ExpectBegin() - mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{})) mock.ExpectExec("INSERT .*").WillReturnError(fmt.Errorf("pop")) mock.ExpectRollback() transactionID := fftypes.NewUUID() - err := s.UpsertTransaction(context.Background(), &fftypes.Transaction{ID: transactionID}) + err := s.InsertTransaction(context.Background(), &fftypes.Transaction{ID: transactionID}) assert.Regexp(t, "FF10116", err) assert.NoError(t, mock.ExpectationsWereMet()) } -func TestUpsertTransactionFailUpdate(t *testing.T) { - s, mock := newMockProvider().init() - transactionID := fftypes.NewUUID() - mock.ExpectBegin() - mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(transactionID.String())) - mock.ExpectExec("UPDATE .*").WillReturnError(fmt.Errorf("pop")) - mock.ExpectRollback() - err := s.UpsertTransaction(context.Background(), &fftypes.Transaction{ID: transactionID}) - assert.Regexp(t, "FF10117", err) - assert.NoError(t, mock.ExpectationsWereMet()) -} - -func TestUpsertTransactionFailCommit(t *testing.T) { +func TestInsertTransactionFailCommit(t *testing.T) { s, mock := newMockProvider().init() transactionID := fftypes.NewUUID() mock.ExpectBegin() - mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id"})) mock.ExpectExec("INSERT .*").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit().WillReturnError(fmt.Errorf("pop")) - err := s.UpsertTransaction(context.Background(), &fftypes.Transaction{ID: transactionID}) + err := s.InsertTransaction(context.Background(), &fftypes.Transaction{ID: transactionID}) assert.Regexp(t, "FF10119", err) assert.NoError(t, mock.ExpectationsWereMet()) } diff --git a/internal/events/batch_pin_complete.go b/internal/events/batch_pin_complete.go index ba6ecbe116..98e2bc51e2 100644 --- a/internal/events/batch_pin_complete.go +++ b/internal/events/batch_pin_complete.go @@ -72,13 +72,8 @@ func (em *eventManager) handlePrivatePinComplete(batchPin *blockchain.BatchPin) } func (em *eventManager) persistBatchTransaction(ctx context.Context, batchPin *blockchain.BatchPin) error { - return em.database.UpsertTransaction(ctx, &fftypes.Transaction{ - ID: batchPin.TransactionID, - Namespace: batchPin.Namespace, - Type: fftypes.TransactionTypeBatchPin, - Status: fftypes.OpStatusSucceeded, - BlockchainIDs: fftypes.NewFFStringArray(batchPin.Event.BlockchainTXID), - }) + _, err := em.txHelper.PersistTransaction(ctx, batchPin.Namespace, batchPin.TransactionID, fftypes.TransactionTypeBatchPin, batchPin.Event.BlockchainTXID) + return err } func (em *eventManager) persistContexts(ctx context.Context, batchPin *blockchain.BatchPin, private bool) error { diff --git a/internal/events/batch_pin_complete_test.go b/internal/events/batch_pin_complete_test.go index c503c64012..053d72c782 100644 --- a/internal/events/batch_pin_complete_test.go +++ b/internal/events/batch_pin_complete_test.go @@ -28,6 +28,7 @@ import ( "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/mocks/publicstoragemocks" + "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" @@ -46,8 +47,9 @@ func TestBatchPinCompleteOkBroadcast(t *testing.T) { BatchPayloadRef: "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD", Contexts: []*fftypes.Bytes32{fftypes.NewRandB32()}, Event: blockchain.Event{ - Name: "BatchPin", - ProtocolID: "tx1", + Name: "BatchPin", + BlockchainTXID: "0x12345", + ProtocolID: "10/20/30", }, } batchData := &fftypes.Batch{ @@ -78,6 +80,12 @@ func TestBatchPinCompleteOkBroadcast(t *testing.T) { MatchedBy(func(pr string) bool { return pr == batch.BatchPayloadRef })). Return(batchReadCloser, nil) + mth := em.txHelper.(*txcommonmocks.Helper) + mth.On("PersistTransaction", mock.Anything, "ns1", batch.TransactionID, fftypes.TransactionTypeBatchPin, "0x12345"). + Return(false, fmt.Errorf("pop")).Once() + mth.On("PersistTransaction", mock.Anything, "ns1", batch.TransactionID, fftypes.TransactionTypeBatchPin, "0x12345"). + Return(true, nil) + mdi := em.database.(*databasemocks.Plugin) rag := mdi.On("RunAsGroup", mock.Anything, mock.Anything).Return(nil) rag.RunFn = func(a mock.Arguments) { @@ -97,8 +105,6 @@ func TestBatchPinCompleteOkBroadcast(t *testing.T) { mdi.On("InsertEvent", mock.Anything, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypeBlockchainEvent })).Return(nil).Times(2) - mdi.On("UpsertTransaction", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")).Once() - mdi.On("UpsertTransaction", mock.Anything, mock.Anything).Return(nil).Once() mdi.On("UpsertPin", mock.Anything, mock.Anything).Return(nil).Once() mdi.On("UpsertBatch", mock.Anything, mock.Anything, false).Return(nil).Once() mbi := &blockchainmocks.Plugin{} @@ -121,6 +127,9 @@ func TestBatchPinCompleteOkPrivate(t *testing.T) { TransactionID: fftypes.NewUUID(), BatchID: fftypes.NewUUID(), Contexts: []*fftypes.Bytes32{fftypes.NewRandB32()}, + Event: blockchain.Event{ + BlockchainTXID: "0x12345", + }, } batchData := &fftypes.Batch{ ID: batch.BatchID, @@ -144,9 +153,11 @@ func TestBatchPinCompleteOkPrivate(t *testing.T) { MatchedBy(func(pr string) bool { return pr == batch.BatchPayloadRef })). Return(batchReadCloser, nil) + mth := em.txHelper.(*txcommonmocks.Helper) + mth.On("PersistTransaction", mock.Anything, "ns1", batch.TransactionID, fftypes.TransactionTypeBatchPin, "0x12345").Return(true, nil) + mdi := em.database.(*databasemocks.Plugin) mdi.On("RunAsGroup", mock.Anything, mock.Anything).Return(nil) - mdi.On("UpsertTransaction", mock.Anything, mock.Anything).Return(nil) mdi.On("UpsertPin", mock.Anything, mock.Anything).Return(nil) mbi := &blockchainmocks.Plugin{} diff --git a/internal/events/event_manager.go b/internal/events/event_manager.go index ebf0926115..ee06536ef3 100644 --- a/internal/events/event_manager.go +++ b/internal/events/event_manager.go @@ -35,6 +35,7 @@ import ( "github.com/hyperledger/firefly/internal/privatemessaging" "github.com/hyperledger/firefly/internal/retry" "github.com/hyperledger/firefly/internal/sysmessaging" + "github.com/hyperledger/firefly/internal/txcommon" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/dataexchange" @@ -78,6 +79,7 @@ type eventManager struct { ni sysmessaging.LocalNodeInfo publicstorage publicstorage.Plugin database database.Plugin + txHelper txcommon.Helper identity identity.Manager definitions definitions.DefinitionHandlers data data.Manager @@ -105,6 +107,7 @@ func NewEventManager(ctx context.Context, ni sysmessaging.LocalNodeInfo, pi publ ni: ni, publicstorage: pi, database: di, + txHelper: txcommon.NewTransactionHelper(di), identity: im, definitions: dh, data: dm, diff --git a/internal/events/event_manager_test.go b/internal/events/event_manager_test.go index a9b47a8204..fa6b222ba6 100644 --- a/internal/events/event_manager_test.go +++ b/internal/events/event_manager_test.go @@ -33,6 +33,7 @@ import ( "github.com/hyperledger/firefly/mocks/privatemessagingmocks" "github.com/hyperledger/firefly/mocks/publicstoragemocks" "github.com/hyperledger/firefly/mocks/sysmessagingmocks" + "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -57,6 +58,7 @@ func newTestEventManager(t *testing.T) (*eventManager, func()) { met.On("Name").Return("ut").Maybe() emi, err := NewEventManager(ctx, mni, mpi, mdi, mim, msh, mdm, mbm, mpm, mam) em := emi.(*eventManager) + em.txHelper = &txcommonmocks.Helper{} rag := mdi.On("RunAsGroup", em.ctx, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { rag.ReturnArguments = mock.Arguments{a[1].(func(context.Context) error)(a[0].(context.Context))} diff --git a/internal/events/operation_update.go b/internal/events/operation_update.go index e54fc8055f..5d6f18be49 100644 --- a/internal/events/operation_update.go +++ b/internal/events/operation_update.go @@ -39,30 +39,15 @@ func (em *eventManager) operationUpdateCtx(ctx context.Context, operationID *fft return err } - setTXFailed := false - // Special handling for OpTypeTokenTransfer, which writes an event when it fails if op.Type == fftypes.OpTypeTokenTransfer && txState == fftypes.OpStatusFailed { - setTXFailed = true event := fftypes.NewEvent(fftypes.EventTypeTransferOpFailed, op.Namespace, op.ID) if err := em.database.InsertEvent(ctx, event); err != nil { return err } } - tx, err := em.database.GetTransactionByID(ctx, op.Transaction) - if tx != nil { - if setTXFailed { - tx.Status = fftypes.OpStatusFailed - } - tx.BlockchainIDs = tx.BlockchainIDs.AppendLowerUnique(blockchainTXID) - err = em.database.UpsertTransaction(ctx, tx) - } - if err != nil { - return err - } - - return nil + return em.txHelper.AddBlockchainTX(ctx, op.Transaction, blockchainTXID) } func (em *eventManager) OperationUpdate(plugin fftypes.Named, operationID *fftypes.UUID, txState fftypes.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) error { diff --git a/internal/events/operation_update_test.go b/internal/events/operation_update_test.go index 876dd550cc..a82c3c673c 100644 --- a/internal/events/operation_update_test.go +++ b/internal/events/operation_update_test.go @@ -23,6 +23,7 @@ import ( "github.com/hyperledger/firefly/mocks/blockchainmocks" "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -33,19 +34,19 @@ func TestOperationUpdateSuccess(t *testing.T) { defer cancel() mdi := em.database.(*databasemocks.Plugin) mbi := &blockchainmocks.Plugin{} + mth := em.txHelper.(*txcommonmocks.Helper) opID := fftypes.NewUUID() txid := fftypes.NewUUID() mdi.On("RunAsGroup", em.ctx, mock.Anything).Run(func(args mock.Arguments) { args[1].(func(ctx context.Context) error)(em.ctx) }).Return(nil) - mdi.On("GetOperationByID", em.ctx, opID).Return(&fftypes.Operation{ID: opID}, nil) + mdi.On("GetOperationByID", em.ctx, opID).Return(&fftypes.Operation{ID: opID, Transaction: txid}, nil) mdi.On("UpdateOperation", em.ctx, opID, mock.Anything).Return(nil) - mdi.On("GetTransactionByID", em.ctx, mock.Anything).Return(&fftypes.Transaction{ID: txid}, nil) - mdi.On("UpsertTransaction", em.ctx, mock.Anything).Return(nil) + mth.On("AddBlockchainTX", mock.Anything, txid, "0x12345").Return(nil) info := fftypes.JSONObject{"some": "info"} - err := em.OperationUpdate(mdi, opID, fftypes.OpStatusFailed, "", "some error", info) + err := em.OperationUpdate(mdi, opID, fftypes.OpStatusFailed, "0x12345", "some error", info) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -76,11 +77,33 @@ func TestOperationUpdateError(t *testing.T) { mbi := &blockchainmocks.Plugin{} opID := fftypes.NewUUID() - mdi.On("GetOperationByID", em.ctx, opID).Return(&fftypes.Operation{ID: opID}, nil) + txid := fftypes.NewUUID() + mdi.On("GetOperationByID", em.ctx, opID).Return(&fftypes.Operation{ID: opID, Transaction: txid}, nil) mdi.On("UpdateOperation", em.ctx, opID, mock.Anything).Return(fmt.Errorf("pop")) info := fftypes.JSONObject{"some": "info"} - err := em.operationUpdateCtx(em.ctx, opID, fftypes.OpStatusFailed, "", "some error", info) + err := em.operationUpdateCtx(em.ctx, opID, fftypes.OpStatusFailed, "0x12345", "some error", info) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) + mbi.AssertExpectations(t) +} + +func TestOperationTXUpdateError(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mbi := &blockchainmocks.Plugin{} + mth := em.txHelper.(*txcommonmocks.Helper) + + opID := fftypes.NewUUID() + txid := fftypes.NewUUID() + mdi.On("GetOperationByID", em.ctx, opID).Return(&fftypes.Operation{ID: opID, Transaction: txid}, nil) + mdi.On("UpdateOperation", em.ctx, opID, mock.Anything).Return(nil) + mth.On("AddBlockchainTX", mock.Anything, txid, "0x12345").Return(fmt.Errorf("pop")) + + info := fftypes.JSONObject{"some": "info"} + err := em.operationUpdateCtx(em.ctx, opID, fftypes.OpStatusFailed, "0x12345", "some error", info) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -92,11 +115,13 @@ func TestOperationUpdateTransferFail(t *testing.T) { defer cancel() mdi := em.database.(*databasemocks.Plugin) mbi := &blockchainmocks.Plugin{} + mth := em.txHelper.(*txcommonmocks.Helper) op := &fftypes.Operation{ - ID: fftypes.NewUUID(), - Type: fftypes.OpTypeTokenTransfer, - Namespace: "ns1", + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenTransfer, + Namespace: "ns1", + Transaction: fftypes.NewUUID(), } mdi.On("GetOperationByID", em.ctx, op.ID).Return(op, nil) @@ -104,11 +129,10 @@ func TestOperationUpdateTransferFail(t *testing.T) { mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypeTransferOpFailed && e.Namespace == "ns1" })).Return(nil) - mdi.On("GetTransactionByID", em.ctx, mock.Anything).Return(&fftypes.Transaction{ID: fftypes.NewUUID()}, nil) - mdi.On("UpsertTransaction", em.ctx, mock.Anything).Return(nil) + mth.On("AddBlockchainTX", mock.Anything, op.Transaction, "0x12345").Return(nil) info := fftypes.JSONObject{"some": "info"} - err := em.operationUpdateCtx(em.ctx, op.ID, fftypes.OpStatusFailed, "", "some error", info) + err := em.operationUpdateCtx(em.ctx, op.ID, fftypes.OpStatusFailed, "0x12345", "some error", info) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -120,21 +144,22 @@ func TestOperationUpdateTransferTransactionFail(t *testing.T) { defer cancel() mdi := em.database.(*databasemocks.Plugin) mbi := &blockchainmocks.Plugin{} + mth := em.txHelper.(*txcommonmocks.Helper) op := &fftypes.Operation{ - ID: fftypes.NewUUID(), - Type: fftypes.OpTypeTokenTransfer, - Namespace: "ns1", + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenTransfer, + Namespace: "ns1", + Transaction: fftypes.NewUUID(), } mdi.On("GetOperationByID", em.ctx, op.ID).Return(op, nil) mdi.On("UpdateOperation", em.ctx, op.ID, mock.Anything).Return(nil) mdi.On("InsertEvent", em.ctx, mock.Anything).Return(nil) - mdi.On("GetTransactionByID", em.ctx, mock.Anything).Return(&fftypes.Transaction{ID: fftypes.NewUUID()}, nil) - mdi.On("UpsertTransaction", em.ctx, mock.Anything).Return(fmt.Errorf("pop")) + mth.On("AddBlockchainTX", mock.Anything, op.Transaction, "0x12345").Return(fmt.Errorf("pop")) info := fftypes.JSONObject{"some": "info"} - err := em.operationUpdateCtx(em.ctx, op.ID, fftypes.OpStatusFailed, "", "some error", info) + err := em.operationUpdateCtx(em.ctx, op.ID, fftypes.OpStatusFailed, "0x12345", "some error", info) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -160,7 +185,7 @@ func TestOperationUpdateTransferEventFail(t *testing.T) { })).Return(fmt.Errorf("pop")) info := fftypes.JSONObject{"some": "info"} - err := em.operationUpdateCtx(em.ctx, op.ID, fftypes.OpStatusFailed, "", "some error", info) + err := em.operationUpdateCtx(em.ctx, op.ID, fftypes.OpStatusFailed, "0x12345", "some error", info) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go index 8e8dfa5501..03bc33005b 100644 --- a/internal/events/token_pool_created.go +++ b/internal/events/token_pool_created.go @@ -41,23 +41,12 @@ func addPoolDetailsFromPlugin(ffPool *fftypes.TokenPool, pluginPool *tokens.Toke } } -func poolTransaction(pool *fftypes.TokenPool, status fftypes.OpStatus, blockchainTXID string) *fftypes.Transaction { - return &fftypes.Transaction{ - ID: pool.TX.ID, - Status: status, - Namespace: pool.Namespace, - Type: pool.TX.Type, - BlockchainIDs: fftypes.NewFFStringArray(blockchainTXID), - } -} - func (em *eventManager) confirmPool(ctx context.Context, pool *fftypes.TokenPool, ev *blockchain.Event, blockchainTXID string) error { chainEvent := buildBlockchainEvent(pool.Namespace, nil, ev, &pool.TX) if err := em.persistBlockchainEvent(ctx, chainEvent); err != nil { return err } - tx := poolTransaction(pool, fftypes.OpStatusSucceeded, blockchainTXID) - if err := em.database.UpsertTransaction(ctx, tx); err != nil { + if _, err := em.txHelper.PersistTransaction(ctx, pool.Namespace, pool.TX.ID, pool.TX.Type, blockchainTXID); err != nil { return err } pool.State = fftypes.TokenPoolStateConfirmed diff --git a/internal/events/token_pool_created_test.go b/internal/events/token_pool_created_test.go index 895a9653d4..b2c1eba1ab 100644 --- a/internal/events/token_pool_created_test.go +++ b/internal/events/token_pool_created_test.go @@ -24,6 +24,7 @@ import ( "github.com/hyperledger/firefly/mocks/broadcastmocks" "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/tokenmocks" + "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/hyperledger/firefly/pkg/tokens" @@ -90,6 +91,7 @@ func TestTokenPoolCreatedConfirm(t *testing.T) { defer cancel() mdi := em.database.(*databasemocks.Plugin) mti := &tokenmocks.Plugin{} + mth := em.txHelper.(*txcommonmocks.Helper) txID := fftypes.NewUUID() info := fftypes.JSONObject{"some": "info"} @@ -129,9 +131,7 @@ func TestTokenPoolCreatedConfirm(t *testing.T) { mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypeBlockchainEvent })).Return(nil).Once() - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil).Once() + mth.On("PersistTransaction", mock.Anything, "ns1", txID, fftypes.TransactionTypeTokenPool, "0xffffeeee").Return(true, nil).Once() mdi.On("UpsertTokenPool", em.ctx, storedPool).Return(nil).Once() mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypePoolConfirmed && *e.Reference == *storedPool.ID @@ -190,6 +190,7 @@ func TestTokenPoolCreatedMigrate(t *testing.T) { mdi := em.database.(*databasemocks.Plugin) mam := em.assets.(*assetmocks.Manager) mti := &tokenmocks.Plugin{} + mth := em.txHelper.(*txcommonmocks.Helper) txID := fftypes.NewUUID() info := fftypes.JSONObject{"some": "info"} @@ -226,9 +227,7 @@ func TestTokenPoolCreatedMigrate(t *testing.T) { mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypeBlockchainEvent })).Return(nil).Once() - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil).Once() + mth.On("PersistTransaction", mock.Anything, "ns1", txID, fftypes.TransactionTypeTokenPool, "0xffffeeee").Return(true, nil).Once() mdi.On("UpsertTokenPool", em.ctx, storedPool).Return(nil).Once() mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypePoolConfirmed && *e.Reference == *storedPool.ID @@ -282,6 +281,7 @@ func TestConfirmPoolTxFail(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() mdi := em.database.(*databasemocks.Plugin) + mth := em.txHelper.(*txcommonmocks.Helper) txID := fftypes.NewUUID() storedPool := &fftypes.TokenPool{ @@ -304,9 +304,7 @@ func TestConfirmPoolTxFail(t *testing.T) { mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypeBlockchainEvent })).Return(nil) - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(fmt.Errorf("pop")) + mth.On("PersistTransaction", mock.Anything, "ns1", txID, fftypes.TransactionTypeTokenPool, "0xffffeeee").Return(false, fmt.Errorf("pop")) err := em.confirmPool(em.ctx, storedPool, event, "0xffffeeee") assert.EqualError(t, err, "pop") @@ -318,6 +316,7 @@ func TestConfirmPoolUpsertFail(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() mdi := em.database.(*databasemocks.Plugin) + mth := em.txHelper.(*txcommonmocks.Helper) txID := fftypes.NewUUID() storedPool := &fftypes.TokenPool{ @@ -340,9 +339,7 @@ func TestConfirmPoolUpsertFail(t *testing.T) { mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypeBlockchainEvent })).Return(nil) - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Type == fftypes.TransactionTypeTokenPool - })).Return(nil) + mth.On("PersistTransaction", mock.Anything, "ns1", txID, fftypes.TransactionTypeTokenPool, "0xffffeeee").Return(true, nil).Once() mdi.On("UpsertTokenPool", em.ctx, storedPool).Return(fmt.Errorf("pop")) err := em.confirmPool(em.ctx, storedPool, event, "0xffffeeee") diff --git a/internal/events/tokens_transferred.go b/internal/events/tokens_transferred.go index 1a4e066455..5fc3666e2f 100644 --- a/internal/events/tokens_transferred.go +++ b/internal/events/tokens_transferred.go @@ -78,15 +78,8 @@ func (em *eventManager) persistTokenTransfer(ctx context.Context, transfer *toke return false, err } - tx := &fftypes.Transaction{ - ID: transfer.TX.ID, - Status: fftypes.OpStatusSucceeded, - Namespace: transfer.Namespace, - Type: transfer.TX.Type, - BlockchainIDs: fftypes.NewFFStringArray(transfer.Event.BlockchainTXID), - } - if err := em.database.UpsertTransaction(ctx, tx); err != nil { - return false, err + if valid, err := em.txHelper.PersistTransaction(ctx, transfer.Namespace, transfer.TX.ID, transfer.TX.Type, transfer.Event.BlockchainTXID); err != nil || !valid { + return valid, err } // Some operations result in multiple transfer events - if the protocol ID was unique but the diff --git a/internal/events/tokens_transferred_test.go b/internal/events/tokens_transferred_test.go index 4054dccd8e..1a7b86bfb7 100644 --- a/internal/events/tokens_transferred_test.go +++ b/internal/events/tokens_transferred_test.go @@ -22,6 +22,7 @@ import ( "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/tokenmocks" + "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/hyperledger/firefly/pkg/tokens" @@ -139,6 +140,7 @@ func TestPersistTransferBadOp(t *testing.T) { defer cancel() mdi := em.database.(*databasemocks.Plugin) + mth := em.txHelper.(*txcommonmocks.Helper) transfer := newTransfer() pool := &fftypes.TokenPool{ @@ -148,14 +150,13 @@ func TestPersistTransferBadOp(t *testing.T) { Input: fftypes.JSONObject{ "id": "bad", }, + Transaction: fftypes.NewUUID(), }} mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil) mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil) mdi.On("GetOperations", em.ctx, mock.Anything).Return(ops, nil, nil) - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(t *fftypes.Transaction) bool { - return *t.ID == *transfer.TX.ID && t.Type == fftypes.TransactionTypeTokenTransfer - })).Return(fmt.Errorf("pop")) + mth.On("PersistTransaction", mock.Anything, "ns1", transfer.TX.ID, fftypes.TransactionTypeTokenTransfer, "0xffffeeee").Return(false, fmt.Errorf("pop")) valid, err := em.persistTokenTransfer(em.ctx, transfer) assert.False(t, valid) @@ -169,6 +170,7 @@ func TestPersistTransferTxFail(t *testing.T) { defer cancel() mdi := em.database.(*databasemocks.Plugin) + mth := em.txHelper.(*txcommonmocks.Helper) transfer := newTransfer() pool := &fftypes.TokenPool{ @@ -184,9 +186,7 @@ func TestPersistTransferTxFail(t *testing.T) { mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil) mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil) mdi.On("GetOperations", em.ctx, mock.Anything).Return(ops, nil, nil) - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(t *fftypes.Transaction) bool { - return *t.ID == *transfer.TX.ID && t.Type == fftypes.TransactionTypeTokenTransfer - })).Return(fmt.Errorf("pop")) + mth.On("PersistTransaction", mock.Anything, "ns1", transfer.TX.ID, fftypes.TransactionTypeTokenTransfer, "0xffffeeee").Return(false, fmt.Errorf("pop")) valid, err := em.persistTokenTransfer(em.ctx, transfer) assert.False(t, valid) @@ -200,6 +200,7 @@ func TestPersistTransferGetTransferFail(t *testing.T) { defer cancel() mdi := em.database.(*databasemocks.Plugin) + mth := em.txHelper.(*txcommonmocks.Helper) transfer := newTransfer() pool := &fftypes.TokenPool{ @@ -215,9 +216,7 @@ func TestPersistTransferGetTransferFail(t *testing.T) { mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil) mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil) mdi.On("GetOperations", em.ctx, mock.Anything).Return(ops, nil, nil) - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(t *fftypes.Transaction) bool { - return *t.ID == *transfer.TX.ID && t.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("PersistTransaction", mock.Anything, "ns1", transfer.TX.ID, fftypes.TransactionTypeTokenTransfer, "0xffffeeee").Return(true, nil) mdi.On("GetTokenTransfer", em.ctx, localID).Return(nil, fmt.Errorf("pop")) valid, err := em.persistTokenTransfer(em.ctx, transfer) @@ -232,6 +231,7 @@ func TestPersistTransferBlockchainEventFail(t *testing.T) { defer cancel() mdi := em.database.(*databasemocks.Plugin) + mth := em.txHelper.(*txcommonmocks.Helper) transfer := newTransfer() pool := &fftypes.TokenPool{ @@ -247,9 +247,7 @@ func TestPersistTransferBlockchainEventFail(t *testing.T) { mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil) mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil) mdi.On("GetOperations", em.ctx, mock.Anything).Return(ops, nil, nil) - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(t *fftypes.Transaction) bool { - return *t.ID == *transfer.TX.ID && t.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("PersistTransaction", mock.Anything, "ns1", transfer.TX.ID, fftypes.TransactionTypeTokenTransfer, "0xffffeeee").Return(true, nil) mdi.On("GetTokenTransfer", em.ctx, localID).Return(nil, nil) mdi.On("InsertBlockchainEvent", em.ctx, mock.MatchedBy(func(e *fftypes.BlockchainEvent) bool { return e.Namespace == pool.Namespace && e.Name == transfer.Event.Name @@ -268,6 +266,7 @@ func TestTokensTransferredWithTransactionRegenerateLocalID(t *testing.T) { mdi := em.database.(*databasemocks.Plugin) mti := &tokenmocks.Plugin{} + mth := em.txHelper.(*txcommonmocks.Helper) transfer := newTransfer() pool := &fftypes.TokenPool{ @@ -283,9 +282,7 @@ func TestTokensTransferredWithTransactionRegenerateLocalID(t *testing.T) { mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil) mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil) mdi.On("GetOperations", em.ctx, mock.Anything).Return(operations, nil, nil) - mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(t *fftypes.Transaction) bool { - return *t.ID == *transfer.TX.ID && t.Type == fftypes.TransactionTypeTokenTransfer - })).Return(nil) + mth.On("PersistTransaction", mock.Anything, "ns1", transfer.TX.ID, fftypes.TransactionTypeTokenTransfer, "0xffffeeee").Return(true, nil) mdi.On("GetTokenTransfer", em.ctx, localID).Return(&fftypes.TokenTransfer{}, nil) mdi.On("InsertBlockchainEvent", em.ctx, mock.MatchedBy(func(e *fftypes.BlockchainEvent) bool { return e.Namespace == pool.Namespace && e.Name == transfer.Event.Name diff --git a/internal/i18n/en_translations.go b/internal/i18n/en_translations.go index 80a6dfcca8..4c1b99c5c2 100644 --- a/internal/i18n/en_translations.go +++ b/internal/i18n/en_translations.go @@ -253,4 +253,5 @@ var ( MsgFFISchemaCompileFail = ffm("FF10333", "Failed compile schema for param '%s'", 400) MsgPluginInitializationFailed = ffm("FF10334", "Plugin initialization error", 500) MsgSafeCharsOnly = ffm("FF10335", "Field '%s' must include only alphanumerics (a-zA-Z0-9), dot (.), dash (-) and underscore (_)", 400) + MsgUnknownTransactionType = ffm("FF10336", "Unknown transaction type '%s'", 400) ) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 8b12f6ae5c..e1d4da0f5e 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -89,6 +89,7 @@ type Orchestrator interface { GetTransactionByID(ctx context.Context, ns, id string) (*fftypes.Transaction, error) GetTransactionOperations(ctx context.Context, ns, id string) ([]*fftypes.Operation, *database.FilterResult, error) GetTransactionBlockchainEvents(ctx context.Context, ns, id string) ([]*fftypes.BlockchainEvent, *database.FilterResult, error) + GetTransactionStatus(ctx context.Context, ns, id string) (*fftypes.TransactionStatus, error) GetTransactions(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.Transaction, *database.FilterResult, error) GetMessageByID(ctx context.Context, ns, id string) (*fftypes.Message, error) GetMessageByIDWithData(ctx context.Context, ns, id string) (*fftypes.MessageInOut, error) diff --git a/internal/orchestrator/txn_status.go b/internal/orchestrator/txn_status.go new file mode 100644 index 0000000000..26732f3495 --- /dev/null +++ b/internal/orchestrator/txn_status.go @@ -0,0 +1,182 @@ +// 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 orchestrator + +import ( + "context" + "sort" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +func updateStatus(result *fftypes.TransactionStatus, newStatus fftypes.OpStatus) { + if result.Status != fftypes.OpStatusFailed && newStatus != fftypes.OpStatusSucceeded { + result.Status = newStatus + } +} + +func pendingPlaceholder(t fftypes.TransactionStatusType) *fftypes.TransactionStatusDetails { + return &fftypes.TransactionStatusDetails{ + Type: t, + Status: fftypes.OpStatusPending, + } +} + +func (or *orchestrator) GetTransactionStatus(ctx context.Context, ns, id string) (*fftypes.TransactionStatus, error) { + result := &fftypes.TransactionStatus{ + Status: fftypes.OpStatusSucceeded, + Details: make([]*fftypes.TransactionStatusDetails, 0), + } + + tx, err := or.GetTransactionByID(ctx, ns, id) + if err != nil { + return nil, err + } else if tx == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + } + + ops, _, err := or.GetTransactionOperations(ctx, ns, id) + if err != nil { + return nil, err + } + for _, op := range ops { + result.Details = append(result.Details, &fftypes.TransactionStatusDetails{ + Status: op.Status, + Type: fftypes.TransactionStatusTypeOperation, + SubType: op.Type.String(), + Timestamp: op.Updated, + ID: op.ID, + Error: op.Error, + Info: op.Output, + }) + updateStatus(result, op.Status) + } + + events, _, err := or.GetTransactionBlockchainEvents(ctx, ns, id) + if err != nil { + return nil, err + } + for _, event := range events { + result.Details = append(result.Details, &fftypes.TransactionStatusDetails{ + Status: fftypes.OpStatusSucceeded, + Type: fftypes.TransactionStatusTypeBlockchainEvent, + SubType: event.Name, + Timestamp: event.Timestamp, + ID: event.ID, + Info: event.Info, + }) + } + + switch tx.Type { + case fftypes.TransactionTypeBatchPin: + if len(events) == 0 { + result.Details = append(result.Details, pendingPlaceholder(fftypes.TransactionStatusTypeBlockchainEvent)) + updateStatus(result, fftypes.OpStatusPending) + } + f := database.BatchQueryFactory.NewFilter(ctx) + switch batches, _, err := or.database.GetBatches(ctx, f.Eq("tx.id", id)); { + case err != nil: + return nil, err + case len(batches) == 0: + result.Details = append(result.Details, pendingPlaceholder(fftypes.TransactionStatusTypeBatch)) + updateStatus(result, fftypes.OpStatusPending) + default: + result.Details = append(result.Details, &fftypes.TransactionStatusDetails{ + Status: fftypes.OpStatusSucceeded, + Type: fftypes.TransactionStatusTypeBatch, + SubType: batches[0].Type.String(), + Timestamp: batches[0].Confirmed, + ID: batches[0].ID, + }) + } + + case fftypes.TransactionTypeTokenPool: + if len(events) == 0 { + result.Details = append(result.Details, pendingPlaceholder(fftypes.TransactionStatusTypeBlockchainEvent)) + updateStatus(result, fftypes.OpStatusPending) + } + f := database.TokenPoolQueryFactory.NewFilter(ctx) + switch pools, _, err := or.database.GetTokenPools(ctx, f.Eq("tx.id", id)); { + case err != nil: + return nil, err + case len(pools) == 0: + result.Details = append(result.Details, pendingPlaceholder(fftypes.TransactionStatusTypeTokenPool)) + updateStatus(result, fftypes.OpStatusPending) + case pools[0].State != fftypes.TokenPoolStateConfirmed: + result.Details = append(result.Details, &fftypes.TransactionStatusDetails{ + Status: fftypes.OpStatusPending, + Type: fftypes.TransactionStatusTypeTokenPool, + SubType: pools[0].Type.String(), + ID: pools[0].ID, + }) + default: + result.Details = append(result.Details, &fftypes.TransactionStatusDetails{ + Status: fftypes.OpStatusSucceeded, + Type: fftypes.TransactionStatusTypeTokenPool, + SubType: pools[0].Type.String(), + Timestamp: pools[0].Created, + ID: pools[0].ID, + }) + } + + case fftypes.TransactionTypeTokenTransfer: + if len(events) == 0 { + result.Details = append(result.Details, pendingPlaceholder(fftypes.TransactionStatusTypeBlockchainEvent)) + updateStatus(result, fftypes.OpStatusPending) + } + f := database.TokenTransferQueryFactory.NewFilter(ctx) + switch transfers, _, err := or.database.GetTokenTransfers(ctx, f.Eq("tx.id", id)); { + case err != nil: + return nil, err + case len(transfers) == 0: + result.Details = append(result.Details, pendingPlaceholder(fftypes.TransactionStatusTypeTokenTransfer)) + updateStatus(result, fftypes.OpStatusPending) + default: + result.Details = append(result.Details, &fftypes.TransactionStatusDetails{ + Status: fftypes.OpStatusSucceeded, + Type: fftypes.TransactionStatusTypeTokenTransfer, + SubType: transfers[0].Type.String(), + Timestamp: transfers[0].Created, + ID: transfers[0].LocalID, + }) + } + + case fftypes.TransactionTypeContractInvoke: + // no blockchain events or other objects + + default: + return nil, i18n.NewError(ctx, i18n.MsgUnknownTransactionType, tx.Type) + } + + // Sort with nil timestamps first (ie Pending), then descending by timestamp + sort.SliceStable(result.Details, func(i, j int) bool { + x := result.Details[i].Timestamp + y := result.Details[j].Timestamp + switch { + case y == nil: + return false + case x == nil: + return true + default: + return x.Time().After(*y.Time()) + } + }) + + return result, nil +} diff --git a/internal/orchestrator/txn_status_test.go b/internal/orchestrator/txn_status_test.go new file mode 100644 index 0000000000..435f390efb --- /dev/null +++ b/internal/orchestrator/txn_status_test.go @@ -0,0 +1,699 @@ +// 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 orchestrator + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func compactJSON(s string) string { + var buf bytes.Buffer + if err := json.Compact(&buf, []byte(s)); err != nil { + panic(err) + } + return buf.String() +} + +func TestGetTransactionStatusBatchPinSuccess(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeBatchPin, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusSucceeded, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Updated: fftypes.UnixTime(0), + Output: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + events := []*fftypes.BlockchainEvent{ + { + Name: "BatchPin", + ID: fftypes.NewUUID(), + Timestamp: fftypes.UnixTime(1), + Info: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + batches := []*fftypes.Batch{ + { + ID: fftypes.NewUUID(), + Type: fftypes.MessageTypeBroadcast, + Confirmed: fftypes.UnixTime(2), + }, + } + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + or.mdi.On("GetBatches", mock.Anything, mock.Anything).Return(batches, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Succeeded", + "details": [ + { + "type": "Batch", + "subtype": "broadcast", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:02Z", + "id": "` + batches[0].ID.String() + `" + }, + { + "type": "BlockchainEvent", + "subtype": "BatchPin", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:01Z", + "id": "` + events[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + }, + { + "type": "Operation", + "subtype": "blockchain_batch_pin", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + ops[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusBatchPinFail(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeBatchPin, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusFailed, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Error: "complete failure", + }, + } + events := []*fftypes.BlockchainEvent{} + batches := []*fftypes.Batch{} + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + or.mdi.On("GetBatches", mock.Anything, mock.Anything).Return(batches, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Failed", + "details": [ + { + "type": "Operation", + "subtype": "blockchain_batch_pin", + "status": "Failed", + "id": "` + ops[0].ID.String() + `", + "error": "complete failure" + }, + { + "type": "BlockchainEvent", + "status": "Pending" + }, + { + "type": "Batch", + "status": "Pending" + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusBatchPinPending(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeBatchPin, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusSucceeded, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeBlockchainBatchPin, + Updated: fftypes.UnixTime(0), + }, + } + events := []*fftypes.BlockchainEvent{} + batches := []*fftypes.Batch{} + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + or.mdi.On("GetBatches", mock.Anything, mock.Anything).Return(batches, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Pending", + "details": [ + { + "type": "BlockchainEvent", + "status": "Pending" + }, + { + "type": "Batch", + "status": "Pending" + }, + { + "type": "Operation", + "subtype": "blockchain_batch_pin", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + ops[0].ID.String() + `" + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusTokenPoolSuccess(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeTokenPool, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusSucceeded, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenCreatePool, + Updated: fftypes.UnixTime(0), + Output: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + events := []*fftypes.BlockchainEvent{ + { + Name: "TokenPool", + ID: fftypes.NewUUID(), + Timestamp: fftypes.UnixTime(0), + Info: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + pools := []*fftypes.TokenPool{ + { + ID: fftypes.NewUUID(), + Type: fftypes.TokenTypeFungible, + Created: fftypes.UnixTime(0), + State: fftypes.TokenPoolStateConfirmed, + }, + } + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + or.mdi.On("GetTokenPools", mock.Anything, mock.Anything).Return(pools, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Succeeded", + "details": [ + { + "type": "Operation", + "subtype": "token_create_pool", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + ops[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + }, + { + "type": "BlockchainEvent", + "subtype": "TokenPool", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + events[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + }, + { + "type": "TokenPool", + "subtype": "fungible", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + pools[0].ID.String() + `" + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusTokenPoolPending(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeTokenPool, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusSucceeded, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenCreatePool, + Output: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + events := []*fftypes.BlockchainEvent{} + pools := []*fftypes.TokenPool{} + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + or.mdi.On("GetTokenPools", mock.Anything, mock.Anything).Return(pools, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Pending", + "details": [ + { + "type": "Operation", + "subtype": "token_create_pool", + "status": "Succeeded", + "id": "` + ops[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + }, + { + "type": "BlockchainEvent", + "status": "Pending" + }, + { + "type": "TokenPool", + "status": "Pending" + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusTokenPoolUnconfirmed(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeTokenPool, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusSucceeded, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenCreatePool, + Output: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + events := []*fftypes.BlockchainEvent{} + pools := []*fftypes.TokenPool{ + { + ID: fftypes.NewUUID(), + Type: fftypes.TokenTypeFungible, + Created: fftypes.UnixTime(0), + State: fftypes.TokenPoolStatePending, + }, + } + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + or.mdi.On("GetTokenPools", mock.Anything, mock.Anything).Return(pools, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Pending", + "details": [ + { + "type": "Operation", + "subtype": "token_create_pool", + "status": "Succeeded", + "id": "` + ops[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + }, + { + "type": "BlockchainEvent", + "status": "Pending" + }, + { + "type": "TokenPool", + "subtype": "fungible", + "status": "Pending", + "id": "` + pools[0].ID.String() + `" + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusTokenTransferSuccess(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeTokenTransfer, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusSucceeded, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenTransfer, + Updated: fftypes.UnixTime(0), + Output: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + events := []*fftypes.BlockchainEvent{ + { + Name: "Mint", + ID: fftypes.NewUUID(), + Timestamp: fftypes.UnixTime(0), + Info: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + transfers := []*fftypes.TokenTransfer{ + { + LocalID: fftypes.NewUUID(), + Type: fftypes.TokenTransferTypeMint, + Created: fftypes.UnixTime(0), + }, + } + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + or.mdi.On("GetTokenTransfers", mock.Anything, mock.Anything).Return(transfers, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Succeeded", + "details": [ + { + "type": "Operation", + "subtype": "token_transfer", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + ops[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + }, + { + "type": "BlockchainEvent", + "subtype": "Mint", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + events[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + }, + { + "type": "TokenTransfer", + "subtype": "mint", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + transfers[0].LocalID.String() + `" + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusTokenTransferPending(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeTokenTransfer, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusSucceeded, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenTransfer, + Output: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + events := []*fftypes.BlockchainEvent{} + transfers := []*fftypes.TokenTransfer{} + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + or.mdi.On("GetTokenTransfers", mock.Anything, mock.Anything).Return(transfers, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Pending", + "details": [ + { + "type": "Operation", + "subtype": "token_transfer", + "status": "Succeeded", + "id": "` + ops[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + }, + { + "type": "BlockchainEvent", + "status": "Pending" + }, + { + "type": "TokenTransfer", + "status": "Pending" + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusContractInvokeSuccess(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeContractInvoke, + } + ops := []*fftypes.Operation{ + { + Status: fftypes.OpStatusSucceeded, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeContractInvoke, + Updated: fftypes.UnixTime(0), + Output: fftypes.JSONObject{"transactionHash": "0x100"}, + }, + } + events := []*fftypes.BlockchainEvent{} + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(ops, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(events, nil, nil) + + status, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.NoError(t, err) + + expectedStatus := compactJSON(`{ + "status": "Succeeded", + "details": [ + { + "type": "Operation", + "subtype": "contract_invoke", + "status": "Succeeded", + "timestamp": "1970-01-01T00:00:00Z", + "id": "` + ops[0].ID.String() + `", + "info": {"transactionHash": "0x100"} + } + ] + }`) + statusJSON, _ := json.Marshal(status) + assert.Equal(t, expectedStatus, string(statusJSON)) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusTXError(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(nil, fmt.Errorf("pop")) + + _, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.EqualError(t, err, "pop") + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusNotFound(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(nil, nil) + + _, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.Regexp(t, "FF10109", err) + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusOpError(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(&fftypes.Transaction{}, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + + _, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.EqualError(t, err, "pop") + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusBlockchainEventError(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(&fftypes.Transaction{}, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(nil, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + + _, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.EqualError(t, err, "pop") + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusBatchError(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeBatchPin, + } + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(nil, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(nil, nil, nil) + or.mdi.On("GetBatches", mock.Anything, mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + + _, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.EqualError(t, err, "pop") + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusPoolError(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeTokenPool, + } + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(nil, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(nil, nil, nil) + or.mdi.On("GetTokenPools", mock.Anything, mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + + _, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.EqualError(t, err, "pop") + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusTransferError(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: fftypes.TransactionTypeTokenTransfer, + } + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(nil, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(nil, nil, nil) + or.mdi.On("GetTokenTransfers", mock.Anything, mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + + _, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.EqualError(t, err, "pop") + + or.mdi.AssertExpectations(t) +} + +func TestGetTransactionStatusUnknownType(t *testing.T) { + or := newTestOrchestrator() + + txID := fftypes.NewUUID() + tx := &fftypes.Transaction{ + Type: "bad", + } + + or.mdi.On("GetTransactionByID", mock.Anything, txID).Return(tx, nil) + or.mdi.On("GetOperations", mock.Anything, mock.Anything).Return(nil, nil, nil) + or.mdi.On("GetBlockchainEvents", mock.Anything, mock.Anything).Return(nil, nil, nil) + + _, err := or.GetTransactionStatus(context.Background(), "ns1", txID.String()) + assert.Regexp(t, "FF10336", err) + + or.mdi.AssertExpectations(t) +} diff --git a/internal/txcommon/txcommon.go b/internal/txcommon/txcommon.go new file mode 100644 index 0000000000..77760a8825 --- /dev/null +++ b/internal/txcommon/txcommon.go @@ -0,0 +1,132 @@ +// 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 txcommon + +import ( + "context" + "strings" + + "github.com/hyperledger/firefly/internal/log" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +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 +} + +type transactionHelper struct { + database database.Plugin +} + +func NewTransactionHelper(di database.Plugin) Helper { + return &transactionHelper{ + database: di, + } +} + +// SubmitNewTransaction is called when there is a new transaction being submitted by the local node +func (t *transactionHelper) SubmitNewTransaction(ctx context.Context, ns string, txType fftypes.TransactionType) (*fftypes.UUID, error) { + + tx := &fftypes.Transaction{ + ID: fftypes.NewUUID(), + Namespace: ns, + Type: txType, + } + + if err := t.database.InsertTransaction(ctx, tx); err != nil { + return nil, err + } + + if err := t.database.InsertEvent(ctx, fftypes.NewEvent(fftypes.EventTypeTransactionSubmitted, tx.Namespace, tx.ID)); err != nil { + return nil, err + } + + return tx.ID, nil +} + +// PersistTransaction is called when we need to ensure a transaction exists in the DB, and optionally associate a new BlockchainTXID to it +func (t *transactionHelper) PersistTransaction(ctx context.Context, ns string, id *fftypes.UUID, txType fftypes.TransactionType, blockchainTXID string) (valid bool, err error) { + + tx, err := t.database.GetTransactionByID(ctx, id) + if err != nil { + return false, err + } + + if tx != nil { + + if tx.Namespace != ns { + log.L(ctx).Errorf("Namespace mismatch for transaction '%s' existing=%s new=%s", tx.ID, tx.Namespace, ns) + return false, nil + } + + if tx.Type != txType { + log.L(ctx).Errorf("Type mismatch for transaction '%s' existing=%s new=%s", tx.ID, tx.Type, txType) + return false, nil + } + + newBlockchainIDs, changed := tx.BlockchainIDs.AddToSortedSet(blockchainTXID) + if !changed { + return true, nil + } + + if err = t.database.UpdateTransaction(ctx, tx.ID, database.TransactionQueryFactory.NewUpdate(ctx).Set("blockchainids", newBlockchainIDs)); err != nil { + return false, err + } + + } else { + + if err = t.database.InsertTransaction(ctx, &fftypes.Transaction{ + ID: id, + Namespace: ns, + Type: txType, + BlockchainIDs: fftypes.NewFFStringArray(strings.ToLower(blockchainTXID)), + }); err != nil { + return false, err + } + + } + + return true, nil +} + +// AddBlockchainTX is called when we know the tranasction should exist, and we don't need any validation +// just want to bolt an extra blockchain TXID on - if it's not there already. +func (t *transactionHelper) AddBlockchainTX(ctx context.Context, id *fftypes.UUID, blockchainTXID string) error { + + tx, err := t.database.GetTransactionByID(ctx, id) + if err != nil { + return err + } + + if tx != nil { + + newBlockchainIDs, changed := tx.BlockchainIDs.AddToSortedSet(blockchainTXID) + if !changed { + return nil + } + + if err = t.database.UpdateTransaction(ctx, tx.ID, database.TransactionQueryFactory.NewUpdate(ctx).Set("blockchainids", newBlockchainIDs)); err != nil { + return err + } + + } + + return nil +} diff --git a/internal/txcommon/txcommon_test.go b/internal/txcommon/txcommon_test.go new file mode 100644 index 0000000000..ea6c161d35 --- /dev/null +++ b/internal/txcommon/txcommon_test.go @@ -0,0 +1,285 @@ +// 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 txcommon + +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 TestSubmitNewTransactionOK(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + var txidInserted *fftypes.UUID + mdi.On("InsertTransaction", ctx, mock.MatchedBy(func(transaction *fftypes.Transaction) bool { + txidInserted = transaction.ID + assert.NotNil(t, transaction.ID) + assert.Equal(t, "ns1", transaction.Namespace) + assert.Equal(t, fftypes.TransactionTypeBatchPin, transaction.Type) + assert.Empty(t, transaction.BlockchainIDs) + return true + })).Return(nil) + mdi.On("InsertEvent", ctx, mock.MatchedBy(func(e *fftypes.Event) bool { + return e.Type == fftypes.EventTypeTransactionSubmitted && e.Reference.Equals(txidInserted) + })).Return(nil) + + txidReturned, err := txHelper.SubmitNewTransaction(ctx, "ns1", fftypes.TransactionTypeBatchPin) + assert.NoError(t, err) + assert.Equal(t, *txidInserted, *txidReturned) + + mdi.AssertExpectations(t) + +} + +func TestSubmitNewTransactionFail(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + mdi.On("InsertTransaction", ctx, mock.Anything).Return(fmt.Errorf("pop")) + + _, err := txHelper.SubmitNewTransaction(ctx, "ns1", fftypes.TransactionTypeBatchPin) + assert.Regexp(t, "pop", err) + + mdi.AssertExpectations(t) + +} + +func TestSubmitNewTransactionEventFail(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + mdi.On("InsertTransaction", ctx, mock.Anything).Return(nil) + mdi.On("InsertEvent", ctx, mock.Anything).Return(fmt.Errorf("pop")) + + _, err := txHelper.SubmitNewTransaction(ctx, "ns1", fftypes.TransactionTypeBatchPin) + assert.Regexp(t, "pop", err) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionNew(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(nil, nil) + mdi.On("InsertTransaction", ctx, mock.MatchedBy(func(transaction *fftypes.Transaction) bool { + assert.Equal(t, txid, transaction.ID) + assert.Equal(t, "ns1", transaction.Namespace) + assert.Equal(t, fftypes.TransactionTypeBatchPin, transaction.Type) + assert.Equal(t, fftypes.FFStringArray{"0x222222"}, transaction.BlockchainIDs) + return true + })).Return(nil) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "0x222222") + assert.NoError(t, err) + assert.True(t, valid) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionNewInserTFail(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(nil, nil) + mdi.On("InsertTransaction", ctx, mock.Anything).Return(fmt.Errorf("pop")) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "0x222222") + assert.Regexp(t, "pop", err) + assert.False(t, valid) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionExistingAddBlockchainID(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(&fftypes.Transaction{ + ID: txid, + Namespace: "ns1", + Type: fftypes.TransactionTypeBatchPin, + Created: fftypes.Now(), + BlockchainIDs: fftypes.FFStringArray{"0x111111"}, + }, nil) + mdi.On("UpdateTransaction", ctx, txid, mock.Anything).Return(nil) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "0x222222") + assert.NoError(t, err) + assert.True(t, valid) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionExistingUpdateFail(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(&fftypes.Transaction{ + ID: txid, + Namespace: "ns1", + Type: fftypes.TransactionTypeBatchPin, + Created: fftypes.Now(), + BlockchainIDs: fftypes.FFStringArray{"0x111111"}, + }, nil) + mdi.On("UpdateTransaction", ctx, txid, mock.Anything).Return(fmt.Errorf("pop")) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "0x222222") + assert.Regexp(t, "pop", err) + assert.False(t, valid) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionExistingNoChange(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(&fftypes.Transaction{ + ID: txid, + Namespace: "ns1", + Type: fftypes.TransactionTypeBatchPin, + Created: fftypes.Now(), + BlockchainIDs: fftypes.FFStringArray{"0x111111"}, + }, nil) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "0x111111") + assert.NoError(t, err) + assert.True(t, valid) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionExistingNoBlockchainID(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(&fftypes.Transaction{ + ID: txid, + Namespace: "ns1", + Type: fftypes.TransactionTypeBatchPin, + Created: fftypes.Now(), + BlockchainIDs: fftypes.FFStringArray{"0x111111"}, + }, nil) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "") + assert.NoError(t, err) + assert.True(t, valid) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionExistingLookupFail(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(nil, fmt.Errorf("pop")) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "") + assert.Regexp(t, "pop", err) + assert.False(t, valid) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionExistingMismatchNS(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(&fftypes.Transaction{ + ID: txid, + Namespace: "ns2", + Type: fftypes.TransactionTypeBatchPin, + Created: fftypes.Now(), + BlockchainIDs: fftypes.FFStringArray{"0x111111"}, + }, nil) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "") + assert.NoError(t, err) + assert.False(t, valid) + + mdi.AssertExpectations(t) + +} + +func TestPersistTransactionExistingMismatchType(t *testing.T) { + + mdi := &databasemocks.Plugin{} + txHelper := NewTransactionHelper(mdi) + ctx := context.Background() + + txid := fftypes.NewUUID() + mdi.On("GetTransactionByID", ctx, txid).Return(&fftypes.Transaction{ + ID: txid, + Namespace: "ns1", + Type: fftypes.TransactionTypeContractInvoke, + Created: fftypes.Now(), + BlockchainIDs: fftypes.FFStringArray{"0x111111"}, + }, nil) + + valid, err := txHelper.PersistTransaction(ctx, "ns1", txid, fftypes.TransactionTypeBatchPin, "") + assert.NoError(t, err) + assert.False(t, valid) + + mdi.AssertExpectations(t) + +} diff --git a/mocks/databasemocks/plugin.go b/mocks/databasemocks/plugin.go index 65d41e5a42..0a4e0a3a41 100644 --- a/mocks/databasemocks/plugin.go +++ b/mocks/databasemocks/plugin.go @@ -2185,6 +2185,20 @@ func (_m *Plugin) InsertOperation(ctx context.Context, operation *fftypes.Operat return r0 } +// InsertTransaction provides a mock function with given fields: ctx, data +func (_m *Plugin) InsertTransaction(ctx context.Context, data *fftypes.Transaction) error { + ret := _m.Called(ctx, data) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Transaction) error); ok { + r0 = rf(ctx, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Name provides a mock function with given fields: func (_m *Plugin) Name() string { ret := _m.Called() @@ -2730,17 +2744,3 @@ func (_m *Plugin) UpsertTokenTransfer(ctx context.Context, transfer *fftypes.Tok return r0 } - -// UpsertTransaction provides a mock function with given fields: ctx, data -func (_m *Plugin) UpsertTransaction(ctx context.Context, data *fftypes.Transaction) error { - ret := _m.Called(ctx, data) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Transaction) error); ok { - r0 = rf(ctx, data) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go index afb4f70f3d..832ecf9c5f 100644 --- a/mocks/orchestratormocks/orchestrator.go +++ b/mocks/orchestratormocks/orchestrator.go @@ -1103,6 +1103,29 @@ func (_m *Orchestrator) GetTransactionOperations(ctx context.Context, ns string, return r0, r1, r2 } +// GetTransactionStatus provides a mock function with given fields: ctx, ns, id +func (_m *Orchestrator) GetTransactionStatus(ctx context.Context, ns string, id string) (*fftypes.TransactionStatus, error) { + ret := _m.Called(ctx, ns, id) + + var r0 *fftypes.TransactionStatus + if rf, ok := ret.Get(0).(func(context.Context, string, string) *fftypes.TransactionStatus); ok { + r0 = rf(ctx, ns, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.TransactionStatus) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, ns, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetTransactions provides a mock function with given fields: ctx, ns, filter func (_m *Orchestrator) GetTransactions(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.Transaction, *database.FilterResult, error) { ret := _m.Called(ctx, ns, filter) diff --git a/mocks/txcommonmocks/helper.go b/mocks/txcommonmocks/helper.go index d9ec62ff04..131a9c10af 100644 --- a/mocks/txcommonmocks/helper.go +++ b/mocks/txcommonmocks/helper.go @@ -14,20 +14,57 @@ type Helper struct { mock.Mock } -// PersistTransaction provides a mock function with given fields: ctx, tx -func (_m *Helper) PersistTransaction(ctx context.Context, tx *fftypes.Transaction) (bool, error) { - ret := _m.Called(ctx, tx) +// AddBlockchainTX provides a mock function with given fields: ctx, id, blockchainTXID +func (_m *Helper) AddBlockchainTX(ctx context.Context, id *fftypes.UUID, blockchainTXID string) error { + ret := _m.Called(ctx, id, blockchainTXID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, string) error); ok { + r0 = rf(ctx, id, blockchainTXID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// PersistTransaction provides a mock function with given fields: ctx, ns, id, txType, blockchainTXID +func (_m *Helper) PersistTransaction(ctx context.Context, ns string, id *fftypes.UUID, txType fftypes.FFEnum, blockchainTXID string) (bool, error) { + ret := _m.Called(ctx, ns, id, txType, blockchainTXID) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Transaction) bool); ok { - r0 = rf(ctx, tx) + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.UUID, fftypes.FFEnum, string) bool); ok { + r0 = rf(ctx, ns, id, txType, blockchainTXID) } else { r0 = ret.Get(0).(bool) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.Transaction) error); ok { - r1 = rf(ctx, tx) + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.UUID, fftypes.FFEnum, string) error); ok { + r1 = rf(ctx, ns, id, txType, blockchainTXID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubmitNewTransaction provides a mock function with given fields: ctx, ns, txType +func (_m *Helper) SubmitNewTransaction(ctx context.Context, ns string, txType fftypes.FFEnum) (*fftypes.UUID, error) { + ret := _m.Called(ctx, ns, txType) + + var r0 *fftypes.UUID + if rf, ok := ret.Get(0).(func(context.Context, string, fftypes.FFEnum) *fftypes.UUID); ok { + r0 = rf(ctx, ns, txType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.UUID) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, fftypes.FFEnum) error); ok { + r1 = rf(ctx, ns, txType) } else { r1 = ret.Error(1) } diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go index b61f494eda..dade44feaf 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -132,8 +132,8 @@ type iBatchCollection interface { } type iTransactionCollection interface { - // UpsertTransaction - Upsert a transaction - UpsertTransaction(ctx context.Context, data *fftypes.Transaction) (err error) + // InsertTransaction - Insert a new transaction + InsertTransaction(ctx context.Context, data *fftypes.Transaction) (err error) // UpdateTransaction - Update transaction UpdateTransaction(ctx context.Context, id *fftypes.UUID, update Update) (err error) @@ -694,7 +694,6 @@ var BatchQueryFactory = &queryFields{ var TransactionQueryFactory = &queryFields{ "id": &UUIDField{}, "type": &StringField{}, - "status": &StringField{}, "created": &TimeField{}, "namespace": &StringField{}, "blockchainids": &FFStringArrayField{}, @@ -863,6 +862,8 @@ var TokenPoolQueryFactory = &queryFields{ "state": &StringField{}, "created": &TimeField{}, "connector": &StringField{}, + "tx.type": &StringField{}, + "tx.id": &UUIDField{}, } // TokenBalanceQueryFactory filter fields for token balances diff --git a/pkg/fftypes/event.go b/pkg/fftypes/event.go index 9d15d79efc..01fbeaa7c6 100644 --- a/pkg/fftypes/event.go +++ b/pkg/fftypes/event.go @@ -20,6 +20,8 @@ package fftypes type EventType = FFEnum var ( + // EventTypeTransactionSubmitted occurs only on the node that initiates a tranaction, when the transaction is submitted + EventTypeTransactionSubmitted EventType = ffEnum("eventtype", "transaction_submitted") // EventTypeMessageConfirmed is the most important event type in the system. This means a message and all of its data // is available for processing by an application. Most applications only need to listen to this event type EventTypeMessageConfirmed EventType = ffEnum("eventtype", "message_confirmed") diff --git a/pkg/fftypes/stringarray.go b/pkg/fftypes/stringarray.go index 144e7aeb94..242315feff 100644 --- a/pkg/fftypes/stringarray.go +++ b/pkg/fftypes/stringarray.go @@ -115,24 +115,30 @@ func (sa FFStringArray) Validate(ctx context.Context, fieldName string, isName b return nil } -func (sa FFStringArray) AppendLowerUnique(s string) FFStringArray { +func (sa FFStringArray) appendLowerIfUnique(s string) FFStringArray { + if s == "" { + return sa + } for _, existing := range sa { - if s == "" || strings.EqualFold(s, existing) { + if strings.EqualFold(s, existing) { return sa } } return append(sa, strings.ToLower(s)) } -// MergeLower returns a new array with a unique set of sorted lower case strings -func (sa FFStringArray) MergeLower(osa FFStringArray) FFStringArray { - res := make(FFStringArray, 0, len(sa)+len(osa)) - for _, s := range sa { - res = res.AppendLowerUnique(s) +// AddToSortedSet determines if the new string is already in the set of strings (case insensitive), +// and if not it adds it to the list (lower case) and returns a new slice of alphabetically sorted +// strings reference and true. +// If no change is made, the original reference is returned and false. +func (sa FFStringArray) AddToSortedSet(newValues ...string) (res FFStringArray, changed bool) { + res = sa + for _, s := range newValues { + res = res.appendLowerIfUnique(s) } - for _, s := range osa { - res = res.AppendLowerUnique(s) + if len(res) != len(sa) { + sort.Strings(res) + return res, true } - sort.Strings(res) - return res + return sa, false } diff --git a/pkg/fftypes/stringarray_test.go b/pkg/fftypes/stringarray_test.go index 459d895db0..c8be55bdb1 100644 --- a/pkg/fftypes/stringarray_test.go +++ b/pkg/fftypes/stringarray_test.go @@ -111,8 +111,18 @@ func TestFFStringArrayScanValue(t *testing.T) { func TestFFStringArrayMergeFold(t *testing.T) { - sa := NewFFStringArray("name2", "NAME1") - assert.Equal(t, FFStringArray{"name1", "name2", "name3"}, sa.MergeLower(FFStringArray{"name3"})) - assert.Equal(t, FFStringArray{"name1", "name2", "name3", "name4"}, sa.MergeLower(FFStringArray{"NAME4", "NAME3", "name1", "name2"})) + sa := NewFFStringArray("name2", "name1") + + nsa, changed := sa.AddToSortedSet("Name3") + assert.True(t, changed) + assert.Equal(t, FFStringArray{"name1", "name2", "name3"}, nsa) + + nsa, changed = sa.AddToSortedSet("name1", "") + assert.False(t, changed) + assert.Equal(t, sa, nsa) + + nsa, changed = sa.AddToSortedSet("NAME4", "NAME3", "name1", "name2") + assert.True(t, changed) + assert.Equal(t, FFStringArray{"name1", "name2", "name3", "name4"}, nsa) } diff --git a/pkg/fftypes/transaction.go b/pkg/fftypes/transaction.go index 97d7ff1538..ccb140d079 100644 --- a/pkg/fftypes/transaction.go +++ b/pkg/fftypes/transaction.go @@ -37,14 +37,37 @@ type TransactionRef struct { ID *UUID `json:"id,omitempty"` } -// Transaction represents (blockchain) transactions that were submitted by this -// node, with the correlation information to look them up on the underlying -// ledger technology +// Transaction is a unit of work sent or received by this node +// It serves as a container for one or more Operations, BlockchainEvents, and other related objects type Transaction struct { ID *UUID `json:"id,omitempty"` Namespace string `json:"namespace,omitempty"` Type TransactionType `json:"type" ffenum:"txtype"` Created *FFTime `json:"created"` - Status OpStatus `json:"status"` BlockchainIDs FFStringArray `json:"blockchainIds,omitempty"` } + +type TransactionStatusType string + +var ( + TransactionStatusTypeOperation TransactionStatusType = "Operation" + TransactionStatusTypeBlockchainEvent TransactionStatusType = "BlockchainEvent" + TransactionStatusTypeBatch TransactionStatusType = "Batch" + TransactionStatusTypeTokenPool TransactionStatusType = "TokenPool" + TransactionStatusTypeTokenTransfer TransactionStatusType = "TokenTransfer" +) + +type TransactionStatusDetails struct { + Type TransactionStatusType `json:"type"` + SubType string `json:"subtype,omitempty"` + Status OpStatus `json:"status"` + Timestamp *FFTime `json:"timestamp,omitempty"` + ID *UUID `json:"id,omitempty"` + Error string `json:"error,omitempty"` + Info JSONObject `json:"info,omitempty"` +} + +type TransactionStatus struct { + Status OpStatus `json:"status"` + Details []*TransactionStatusDetails `json:"details"` +}