diff --git a/db/migrations/postgres/000028_add_tokenpool_fields.down.sql b/db/migrations/postgres/000028_add_tokenpool_fields.down.sql index 78d77fd5b4..70b5e11b5d 100644 --- a/db/migrations/postgres/000028_add_tokenpool_fields.down.sql +++ b/db/migrations/postgres/000028_add_tokenpool_fields.down.sql @@ -1,4 +1,7 @@ BEGIN; +DROP INDEX tokenpool_protocolid; +CREATE UNIQUE INDEX tokenpool_protocolid ON tokenpool(protocol_id); + ALTER TABLE tokenpool DROP COLUMN connector; ALTER TABLE tokenpool DROP COLUMN symbol; ALTER TABLE tokenpool DROP COLUMN message_id; diff --git a/db/migrations/postgres/000028_add_tokenpool_fields.up.sql b/db/migrations/postgres/000028_add_tokenpool_fields.up.sql index aa229c52ae..42283f35e3 100644 --- a/db/migrations/postgres/000028_add_tokenpool_fields.up.sql +++ b/db/migrations/postgres/000028_add_tokenpool_fields.up.sql @@ -3,4 +3,7 @@ DELETE FROM tokenpool; ALTER TABLE tokenpool ADD COLUMN connector VARCHAR(64) NOT NULL; ALTER TABLE tokenpool ADD COLUMN symbol VARCHAR(64); ALTER TABLE tokenpool ADD COLUMN message_id UUID; + +DROP INDEX tokenpool_protocolid; +CREATE UNIQUE INDEX tokenpool_protocolid ON tokenpool(connector,protocol_id); COMMIT; diff --git a/db/migrations/postgres/000033_add_token_connector_fields.down.sql b/db/migrations/postgres/000033_add_token_connector_fields.down.sql index 5401621393..ecb1111ba4 100644 --- a/db/migrations/postgres/000033_add_token_connector_fields.down.sql +++ b/db/migrations/postgres/000033_add_token_connector_fields.down.sql @@ -1,4 +1,7 @@ BEGIN; +DROP INDEX tokentransfer_protocolid; +CREATE UNIQUE INDEX tokentransfer_protocolid ON tokentransfer(protocol_id); + ALTER TABLE tokenaccount DROP COLUMN connector; ALTER TABLE tokentransfer DROP COLUMN connector; COMMIT; diff --git a/db/migrations/postgres/000033_add_token_connector_fields.up.sql b/db/migrations/postgres/000033_add_token_connector_fields.up.sql index cb2854fc70..2480af50ba 100644 --- a/db/migrations/postgres/000033_add_token_connector_fields.up.sql +++ b/db/migrations/postgres/000033_add_token_connector_fields.up.sql @@ -14,4 +14,7 @@ UPDATE tokentransfer SET connector = pool.connector ALTER TABLE tokenaccount ALTER COLUMN connector SET NOT NULL; ALTER TABLE tokentransfer ALTER COLUMN connector SET NOT NULL; +DROP INDEX tokentransfer_protocolid; +CREATE UNIQUE INDEX tokentransfer_protocolid ON tokentransfer(connector,protocol_id); + COMMIT; diff --git a/db/migrations/postgres/000044_add_tokenpool_state.down.sql b/db/migrations/postgres/000044_add_tokenpool_state.down.sql new file mode 100644 index 0000000000..7899e34119 --- /dev/null +++ b/db/migrations/postgres/000044_add_tokenpool_state.down.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE tokenpool DROP COLUMN state; +COMMIT; diff --git a/db/migrations/postgres/000044_add_tokenpool_state.up.sql b/db/migrations/postgres/000044_add_tokenpool_state.up.sql new file mode 100644 index 0000000000..0fde5e8514 --- /dev/null +++ b/db/migrations/postgres/000044_add_tokenpool_state.up.sql @@ -0,0 +1,5 @@ +BEGIN; +ALTER TABLE tokenpool ADD COLUMN state VARCHAR(64); +UPDATE tokenpool SET state='unknown'; +ALTER TABLE tokenpool ALTER COLUMN state SET NOT NULL; +COMMIT; diff --git a/db/migrations/postgres/000045_add_tokentransfer_messageid.down.sql b/db/migrations/postgres/000045_add_tokentransfer_messageid.down.sql new file mode 100644 index 0000000000..440af204c5 --- /dev/null +++ b/db/migrations/postgres/000045_add_tokentransfer_messageid.down.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE tokentransfer DROP COLUMN message_id; +COMMIT; diff --git a/db/migrations/postgres/000045_add_tokentransfer_messageid.up.sql b/db/migrations/postgres/000045_add_tokentransfer_messageid.up.sql new file mode 100644 index 0000000000..79efa1b4e1 --- /dev/null +++ b/db/migrations/postgres/000045_add_tokentransfer_messageid.up.sql @@ -0,0 +1,9 @@ +BEGIN; + +ALTER TABLE tokentransfer ADD COLUMN message_id UUID; + +UPDATE tokentransfer SET message_id = message.id + FROM (SELECT hash, id FROM messages) AS message + WHERE tokentransfer.message_hash = message.hash; + +COMMIT; diff --git a/db/migrations/sqlite/000028_add_tokenpool_fields.down.sql b/db/migrations/sqlite/000028_add_tokenpool_fields.down.sql index ff46885b67..8872f11079 100644 --- a/db/migrations/sqlite/000028_add_tokenpool_fields.down.sql +++ b/db/migrations/sqlite/000028_add_tokenpool_fields.down.sql @@ -1,3 +1,6 @@ +DROP INDEX tokenpool_protocolid; +CREATE UNIQUE INDEX tokenpool_protocolid ON tokenpool(protocol_id); + ALTER TABLE tokenpool DROP COLUMN connector; ALTER TABLE tokenpool DROP COLUMN symbol; ALTER TABLE tokenpool DROP COLUMN message_id; diff --git a/db/migrations/sqlite/000028_add_tokenpool_fields.up.sql b/db/migrations/sqlite/000028_add_tokenpool_fields.up.sql index b2dddf685f..9a21eb8ad1 100644 --- a/db/migrations/sqlite/000028_add_tokenpool_fields.up.sql +++ b/db/migrations/sqlite/000028_add_tokenpool_fields.up.sql @@ -2,3 +2,6 @@ DELETE FROM tokenpool; ALTER TABLE tokenpool ADD COLUMN connector VARCHAR(64) NOT NULL; ALTER TABLE tokenpool ADD COLUMN symbol VARCHAR(64); ALTER TABLE tokenpool ADD COLUMN message_id UUID; + +DROP INDEX tokenpool_protocolid; +CREATE UNIQUE INDEX tokenpool_protocolid ON tokenpool(connector,protocol_id); diff --git a/db/migrations/sqlite/000033_add_token_connector_fields.down.sql b/db/migrations/sqlite/000033_add_token_connector_fields.down.sql index ec7b622f31..71260efe6b 100644 --- a/db/migrations/sqlite/000033_add_token_connector_fields.down.sql +++ b/db/migrations/sqlite/000033_add_token_connector_fields.down.sql @@ -1,2 +1,5 @@ +DROP INDEX tokentransfer_protocolid; +CREATE UNIQUE INDEX tokentransfer_protocolid ON tokentransfer(protocol_id); + ALTER TABLE tokenaccount DROP COLUMN connector; ALTER TABLE tokentransfer DROP COLUMN connector; diff --git a/db/migrations/sqlite/000033_add_token_connector_fields.up.sql b/db/migrations/sqlite/000033_add_token_connector_fields.up.sql index ed8ab7437a..6cfb45e01d 100644 --- a/db/migrations/sqlite/000033_add_token_connector_fields.up.sql +++ b/db/migrations/sqlite/000033_add_token_connector_fields.up.sql @@ -8,3 +8,6 @@ UPDATE tokenaccount SET connector = pool.connector UPDATE tokentransfer SET connector = pool.connector FROM (SELECT protocol_id, connector FROM tokenpool) AS pool WHERE tokentransfer.pool_protocol_id = pool.protocol_id; + +DROP INDEX tokentransfer_protocolid; +CREATE UNIQUE INDEX tokentransfer_protocolid ON tokentransfer(connector,protocol_id); diff --git a/db/migrations/sqlite/000044_add_tokenpool_state.down.sql b/db/migrations/sqlite/000044_add_tokenpool_state.down.sql new file mode 100644 index 0000000000..0fd918cb0d --- /dev/null +++ b/db/migrations/sqlite/000044_add_tokenpool_state.down.sql @@ -0,0 +1 @@ +ALTER TABLE tokenpool DROP COLUMN state; diff --git a/db/migrations/sqlite/000044_add_tokenpool_state.up.sql b/db/migrations/sqlite/000044_add_tokenpool_state.up.sql new file mode 100644 index 0000000000..6aeb17a2f1 --- /dev/null +++ b/db/migrations/sqlite/000044_add_tokenpool_state.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE tokenpool ADD COLUMN state VARCHAR(64); +UPDATE tokenpool SET state="unknown"; diff --git a/db/migrations/sqlite/000045_add_tokentransfer_messageid.down.sql b/db/migrations/sqlite/000045_add_tokentransfer_messageid.down.sql new file mode 100644 index 0000000000..b29958fbb4 --- /dev/null +++ b/db/migrations/sqlite/000045_add_tokentransfer_messageid.down.sql @@ -0,0 +1 @@ +ALTER TABLE tokentransfer DROP COLUMN message_id; diff --git a/db/migrations/sqlite/000045_add_tokentransfer_messageid.up.sql b/db/migrations/sqlite/000045_add_tokentransfer_messageid.up.sql new file mode 100644 index 0000000000..d8e582fe96 --- /dev/null +++ b/db/migrations/sqlite/000045_add_tokentransfer_messageid.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE tokentransfer ADD COLUMN message_id UUID; + +UPDATE tokentransfer SET message_id = message.id + FROM (SELECT hash, id FROM messages) AS message + WHERE tokentransfer.message_hash = message.hash; diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 45b8ba2b75..906f94e3dd 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -4197,6 +4197,11 @@ paths: name: standard schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: state + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: symbol @@ -4266,6 +4271,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -4355,6 +4362,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -4390,6 +4399,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -4460,6 +4471,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -4759,6 +4772,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -4793,6 +4807,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -4976,6 +4991,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5010,6 +5026,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5094,6 +5111,11 @@ paths: name: localid schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: message + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: messagehash @@ -5174,6 +5196,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5357,6 +5380,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5391,6 +5415,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5881,6 +5906,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5915,6 +5941,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6118,6 +6145,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6152,6 +6180,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6238,6 +6267,11 @@ paths: name: standard schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: state + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: symbol @@ -6307,6 +6341,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -6391,6 +6427,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -6426,6 +6464,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -6489,6 +6529,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -6557,6 +6599,11 @@ paths: name: localid schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: message + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: messagehash @@ -6637,6 +6684,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6807,6 +6855,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6841,6 +6890,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6903,6 +6953,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string diff --git a/internal/apiserver/route_post_token_pool.go b/internal/apiserver/route_post_token_pool.go index 5892ab8d4c..31922ca8d1 100644 --- a/internal/apiserver/route_post_token_pool.go +++ b/internal/apiserver/route_post_token_pool.go @@ -39,7 +39,7 @@ var postTokenPool = &oapispec.Route{ FilterFactory: nil, Description: i18n.MsgTBD, JSONInputValue: func() interface{} { return &fftypes.TokenPool{} }, - JSONInputMask: []string{"ID", "Namespace", "Standard", "ProtocolID", "TX", "Message", "Created"}, + JSONInputMask: []string{"ID", "Namespace", "Standard", "ProtocolID", "TX", "Message", "State", "Created"}, JSONOutputValue: func() interface{} { return &fftypes.TokenPool{} }, JSONOutputCodes: []int{http.StatusAccepted, http.StatusOK}, JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { diff --git a/internal/apiserver/route_post_token_pool_by_type.go b/internal/apiserver/route_post_token_pool_by_type.go index 82d0a82c52..cdd98f6380 100644 --- a/internal/apiserver/route_post_token_pool_by_type.go +++ b/internal/apiserver/route_post_token_pool_by_type.go @@ -40,7 +40,7 @@ var postTokenPoolByType = &oapispec.Route{ FilterFactory: nil, Description: i18n.MsgTBD, JSONInputValue: func() interface{} { return &fftypes.TokenPool{} }, - JSONInputMask: []string{"ID", "Namespace", "Standard", "ProtocolID", "TX", "Connector", "Message", "Created"}, + JSONInputMask: []string{"ID", "Namespace", "Standard", "ProtocolID", "TX", "Connector", "Message", "State", "Created"}, JSONOutputValue: func() interface{} { return &fftypes.TokenPool{} }, JSONOutputCodes: []int{http.StatusAccepted, http.StatusOK}, JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { diff --git a/internal/assets/manager.go b/internal/assets/manager.go index b5a6a4e9fa..8921bf6917 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -36,10 +36,10 @@ import ( type Manager interface { CreateTokenPool(ctx context.Context, ns string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) + ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, tx *fftypes.Transaction) error GetTokenPools(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) GetTokenPool(ctx context.Context, ns, connector, poolName string) (*fftypes.TokenPool, error) GetTokenPoolByNameOrID(ctx context.Context, ns string, poolNameOrID string) (*fftypes.TokenPool, error) - ValidateTokenPoolTx(ctx context.Context, pool *fftypes.TokenPool, protocolTxID string) error GetTokenBalances(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenBalance, *database.FilterResult, error) GetTokenAccounts(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenAccount, *database.FilterResult, error) @@ -55,9 +55,6 @@ type Manager interface { GetTokenConnectors(ctx context.Context, ns string) ([]*fftypes.TokenConnector, error) - // Bound token callbacks - TokenPoolCreated(ti tokens.Plugin, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error - // Deprecated CreateTokenPoolByType(ctx context.Context, ns, connector string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) GetTokenPoolsByType(ctx context.Context, ns, connector string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) diff --git a/internal/assets/token_pool.go b/internal/assets/token_pool.go index 594d599460..4e7f08048f 100644 --- a/internal/assets/token_pool.go +++ b/internal/assets/token_pool.go @@ -18,37 +18,13 @@ package assets import ( "context" - "fmt" "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/internal/txcommon" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" ) -func addTokenPoolCreateInputs(op *fftypes.Operation, pool *fftypes.TokenPool) { - op.Input = fftypes.JSONObject{ - "id": pool.ID.String(), - "namespace": pool.Namespace, - "name": pool.Name, - "config": pool.Config, - } -} - -func retrieveTokenPoolCreateInputs(ctx context.Context, op *fftypes.Operation, pool *fftypes.TokenPool) (err error) { - input := &op.Input - pool.ID, err = fftypes.ParseUUID(ctx, input.GetString("id")) - if err != nil { - return err - } - pool.Namespace = input.GetString("namespace") - pool.Name = input.GetString("name") - if pool.Namespace == "" || pool.Name == "" { - return fmt.Errorf("namespace or name missing from inputs") - } - pool.Config = input.GetObject("config") - return nil -} - func (am *assetManager) CreateTokenPool(ctx context.Context, ns string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) { if err := am.data.VerifyNamespaceExists(ctx, ns); err != nil { return nil, err @@ -130,7 +106,7 @@ func (am *assetManager) createTokenPoolInternal(ctx context.Context, pool *fftyp "", fftypes.OpTypeTokenCreatePool, fftypes.OpStatusPending) - addTokenPoolCreateInputs(op, pool) + txcommon.AddTokenPoolCreateInputs(op, pool) err = am.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { err = am.database.UpsertTransaction(ctx, tx, false /* should be new, or idempotent replay */) @@ -146,6 +122,14 @@ func (am *assetManager) createTokenPoolInternal(ctx context.Context, pool *fftyp return pool, plugin.CreateTokenPool(ctx, op.ID, pool) } +func (am *assetManager) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, tx *fftypes.Transaction) error { + plugin, err := am.selectTokenPlugin(ctx, pool.Connector) + if err != nil { + return err + } + return plugin.ActivateTokenPool(ctx, nil, pool, tx) +} + func (am *assetManager) GetTokenPools(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) { if err := fftypes.ValidateFFNameField(ctx, ns, "namespace"); err != nil { return nil, nil, err @@ -206,8 +190,3 @@ func (am *assetManager) GetTokenPoolByNameOrID(ctx context.Context, ns, poolName } return pool, nil } - -func (am *assetManager) ValidateTokenPoolTx(ctx context.Context, pool *fftypes.TokenPool, protocolTxID string) error { - // TODO: validate that the given token pool was created with the given protocolTxId - return nil -} diff --git a/internal/assets/token_pool_created.go b/internal/assets/token_pool_created.go deleted file mode 100644 index 34309318f8..0000000000 --- a/internal/assets/token_pool_created.go +++ /dev/null @@ -1,99 +0,0 @@ -// 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 assets - -import ( - "context" - - "github.com/hyperledger/firefly/internal/log" - "github.com/hyperledger/firefly/pkg/database" - "github.com/hyperledger/firefly/pkg/fftypes" - "github.com/hyperledger/firefly/pkg/tokens" -) - -func (am *assetManager) TokenPoolCreated(tk tokens.Plugin, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { - announce := &fftypes.TokenPoolAnnouncement{ - TokenPool: *pool, - ProtocolTxID: protocolTxID, - } - pool = &announce.TokenPool - - var valid bool - err := am.retry.Do(am.ctx, "persist token pool transaction", func(attempt int) (bool, error) { - err := am.database.RunAsGroup(am.ctx, func(ctx context.Context) error { - // Find a matching operation within this transaction - fb := database.OperationQueryFactory.NewFilter(ctx) - filter := fb.And( - fb.Eq("tx", pool.TX.ID), - fb.Eq("type", fftypes.OpTypeTokenCreatePool), - ) - operations, _, err := am.database.GetOperations(ctx, filter) - if err != nil || len(operations) == 0 { - log.L(ctx).Debugf("Token pool transaction '%s' ignored, as it did not match an operation submitted by this node", pool.TX.ID) - return nil - } - - err = retrieveTokenPoolCreateInputs(ctx, operations[0], pool) - if err != nil { - log.L(ctx).Errorf("Error retrieving pool info from transaction '%s' (%s) - ignoring: %v", pool.TX.ID, err, operations[0].Input) - return nil - } - - // Update the transaction with the info received (but leave transaction as "pending"). - // At this point we are the only node in the network that knows about this transaction object. - // Our local token connector has performed whatever actions it needs to perform, to give us - // enough information to distribute to all other token connectors in the network. - // (e.g. details of a newly created token instance or an existing one) - transaction := &fftypes.Transaction{ - ID: pool.TX.ID, - Status: fftypes.OpStatusPending, - Subject: fftypes.TransactionSubject{ - Namespace: pool.Namespace, - Type: pool.TX.Type, - Signer: pool.Key, - Reference: pool.ID, - }, - ProtocolID: protocolTxID, - Info: additionalInfo, - } - - // Add a new operation for the announcement - op := fftypes.NewTXOperation( - tk, - pool.Namespace, - transaction.ID, - "", - fftypes.OpTypeTokenAnnouncePool, - fftypes.OpStatusPending) - - valid, err = am.txhelper.PersistTransaction(ctx, transaction) - if valid && err == nil { - err = am.database.UpsertOperation(ctx, op, false) - } - return err - }) - return err != nil, err - }) - - if !valid || err != nil { - return err - } - - // Announce the details of the new token pool - _, err = am.broadcast.BroadcastTokenPool(am.ctx, pool.Namespace, announce, false) - return err -} diff --git a/internal/assets/token_pool_created_test.go b/internal/assets/token_pool_created_test.go deleted file mode 100644 index 0eee43e12f..0000000000 --- a/internal/assets/token_pool_created_test.go +++ /dev/null @@ -1,226 +0,0 @@ -// 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 assets - -import ( - "testing" - - "github.com/hyperledger/firefly/mocks/broadcastmocks" - "github.com/hyperledger/firefly/mocks/databasemocks" - "github.com/hyperledger/firefly/mocks/tokenmocks" - "github.com/hyperledger/firefly/pkg/database" - "github.com/hyperledger/firefly/pkg/fftypes" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestTokenPoolCreatedSuccess(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - mdi := am.database.(*databasemocks.Plugin) - mti := &tokenmocks.Plugin{} - mbm := am.broadcast.(*broadcastmocks.Manager) - - poolID := fftypes.NewUUID() - txID := fftypes.NewUUID() - operations := []*fftypes.Operation{ - { - ID: fftypes.NewUUID(), - Input: fftypes.JSONObject{ - "id": poolID.String(), - "namespace": "test-ns", - "name": "my-pool", - }, - }, - } - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, - } - - mti.On("Name").Return("mock-tokens") - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) - mdi.On("GetTransactionByID", mock.Anything, txID).Return(nil, nil) - mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenPool - }), false).Return(nil) - mdi.On("UpsertOperation", am.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { - return op.Type == fftypes.OpTypeTokenAnnouncePool - }), false).Return(nil) - mbm.On("BroadcastTokenPool", am.ctx, "test-ns", mock.MatchedBy(func(pool *fftypes.TokenPoolAnnouncement) bool { - return pool.Namespace == "test-ns" && pool.Name == "my-pool" && *pool.ID == *poolID - }), false).Return(nil, nil) - - info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, pool, "tx1", info) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mbm.AssertExpectations(t) -} - -func TestTokenPoolCreatedOpNotFound(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - mdi := am.database.(*databasemocks.Plugin) - mti := &tokenmocks.Plugin{} - mbm := am.broadcast.(*broadcastmocks.Manager) - - txID := fftypes.NewUUID() - operations := []*fftypes.Operation{} - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, - } - - mti.On("Name").Return("mock-tokens") - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) - - info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, pool, "tx1", info) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mbm.AssertExpectations(t) -} - -func TestTokenPoolMissingID(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - mdi := am.database.(*databasemocks.Plugin) - mti := &tokenmocks.Plugin{} - mbm := am.broadcast.(*broadcastmocks.Manager) - - txID := fftypes.NewUUID() - operations := []*fftypes.Operation{ - { - ID: fftypes.NewUUID(), - Input: fftypes.JSONObject{}, - }, - } - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, - } - - mti.On("Name").Return("mock-tokens") - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) - - info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, pool, "tx1", info) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mbm.AssertExpectations(t) -} - -func TestTokenPoolCreatedMissingNamespace(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - mdi := am.database.(*databasemocks.Plugin) - mti := &tokenmocks.Plugin{} - mbm := am.broadcast.(*broadcastmocks.Manager) - - poolID := fftypes.NewUUID() - txID := fftypes.NewUUID() - operations := []*fftypes.Operation{ - { - ID: fftypes.NewUUID(), - Input: fftypes.JSONObject{ - "id": poolID.String(), - }, - }, - } - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, - } - - mti.On("Name").Return("mock-tokens") - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) - - info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, pool, "tx1", info) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mbm.AssertExpectations(t) -} - -func TestTokenPoolCreatedUpsertFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - mdi := am.database.(*databasemocks.Plugin) - mti := &tokenmocks.Plugin{} - mbm := am.broadcast.(*broadcastmocks.Manager) - - poolID := fftypes.NewUUID() - txID := fftypes.NewUUID() - operations := []*fftypes.Operation{ - { - ID: fftypes.NewUUID(), - Input: fftypes.JSONObject{ - "id": poolID.String(), - "namespace": "test-ns", - "name": "my-pool", - }, - }, - } - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, - } - - mti.On("Name").Return("mock-tokens") - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) - mdi.On("GetTransactionByID", mock.Anything, txID).Return(nil, nil) - mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenPool - }), false).Return(database.HashMismatch) - - info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, pool, "tx1", info) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mbm.AssertExpectations(t) -} diff --git a/internal/assets/token_pool_test.go b/internal/assets/token_pool_test.go index 42efb5ba7d..cf4d029188 100644 --- a/internal/assets/token_pool_test.go +++ b/internal/assets/token_pool_test.go @@ -425,6 +425,42 @@ func TestCreateTokenPoolByTypeConfirm(t *testing.T) { assert.NoError(t, err) } +func TestActivateTokenPool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + pool := &fftypes.TokenPool{ + Namespace: "ns1", + Connector: "magic-tokens", + } + tx := &fftypes.Transaction{} + + mdm := am.data.(*datamocks.Manager) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mti.On("ActivateTokenPool", context.Background(), mock.Anything, pool, tx).Return(nil) + + err := am.ActivateTokenPool(context.Background(), pool, tx) + assert.NoError(t, err) +} + +func TestActivateTokenPoolBadConnector(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + pool := &fftypes.TokenPool{ + Namespace: "ns1", + Connector: "bad", + } + tx := &fftypes.Transaction{} + + mdm := am.data.(*datamocks.Manager) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + + err := am.ActivateTokenPool(context.Background(), pool, tx) + assert.Regexp(t, "FF10272", err) +} + func TestGetTokenPool(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() @@ -582,11 +618,3 @@ func TestGetTokenPoolsByTypeBadNamespace(t *testing.T) { _, _, err := am.GetTokenPoolsByType(context.Background(), "", "magic-tokens", f) assert.Regexp(t, "FF10131", err) } - -func TestValidateTokenPoolTx(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - err := am.ValidateTokenPoolTx(context.Background(), nil, "") - assert.NoError(t, err) -} diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go index 18c46144d3..38c5bf6f34 100644 --- a/internal/assets/token_transfer.go +++ b/internal/assets/token_transfer.go @@ -221,7 +221,8 @@ func (s *transferSender) resolve(ctx context.Context) error { if err = sender.Prepare(ctx); err != nil { return err } - s.transfer.MessageHash = s.transfer.Message.Hash + s.transfer.TokenTransfer.Message = s.transfer.Message.Header.ID + s.transfer.TokenTransfer.MessageHash = s.transfer.Message.Hash } return nil } @@ -274,6 +275,9 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) er if err != nil { return err } + if pool.State != fftypes.TokenPoolStateConfirmed { + return i18n.NewError(ctx, i18n.MsgTokenPoolNotConfirmed) + } err = s.mgr.database.UpsertTransaction(ctx, tx, false /* should be new, or idempotent replay */) if err != nil { diff --git a/internal/assets/token_transfer_test.go b/internal/assets/token_transfer_test.go index b7e8342b3a..228445790f 100644 --- a/internal/assets/token_transfer_test.go +++ b/internal/assets/token_transfer_test.go @@ -118,6 +118,7 @@ func TestMintTokensSuccess(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -147,6 +148,7 @@ func TestMintTokenUnknownConnectorSuccess(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -262,6 +264,7 @@ func TestMintTokenUnknownPoolSuccess(t *testing.T) { { Name: "pool1", ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, }, } totalCount := int64(1) @@ -438,6 +441,7 @@ func TestMintTokensFail(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -467,6 +471,7 @@ func TestMintTokensOperationFail(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -494,6 +499,7 @@ func TestMintTokensConfirm(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -535,6 +541,7 @@ func TestMintTokensByTypeSuccess(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -564,6 +571,7 @@ func TestBurnTokensSuccess(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -615,6 +623,7 @@ func TestBurnTokensConfirm(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -656,6 +665,7 @@ func TestBurnTokensByTypeSuccess(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -691,6 +701,7 @@ func TestTransferTokensSuccess(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -712,6 +723,35 @@ func TestTransferTokensSuccess(t *testing.T) { mti.AssertExpectations(t) } +func TestTransferTokensUnconfirmedPool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + Amount: *fftypes.NewBigInt(5), + }, + Pool: "pool1", + } + pool := &fftypes.TokenPool{ + ProtocolID: "F1", + State: fftypes.TokenPoolStatePending, + } + + mdi := am.database.(*databasemocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil) + + _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) + assert.Regexp(t, "FF10293", err) + + mim.AssertExpectations(t) + mdi.AssertExpectations(t) +} + func TestTransferTokensIdentityFail(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() @@ -764,6 +804,7 @@ func TestTransferTokensInvalidType(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -797,6 +838,7 @@ func TestTransferTokensTransactionFail(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -818,6 +860,7 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() + msgID := fftypes.NewUUID() hash := fftypes.NewRandB32() transfer := &fftypes.TokenTransferInput{ TokenTransfer: fftypes.TokenTransfer{ @@ -828,6 +871,9 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { Pool: "pool1", Message: &fftypes.MessageInOut{ Message: fftypes.Message{ + Header: fftypes.MessageHeader{ + ID: msgID, + }, Hash: hash, }, InlineData: fftypes.InlineData{ @@ -839,6 +885,7 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -861,7 +908,8 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) assert.NoError(t, err) - assert.Equal(t, *hash, *transfer.MessageHash) + assert.Equal(t, *msgID, *transfer.TokenTransfer.Message) + assert.Equal(t, *hash, *transfer.TokenTransfer.MessageHash) mbm.AssertExpectations(t) mim.AssertExpectations(t) @@ -909,6 +957,7 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() + msgID := fftypes.NewUUID() hash := fftypes.NewRandB32() transfer := &fftypes.TokenTransferInput{ TokenTransfer: fftypes.TokenTransfer{ @@ -920,6 +969,7 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { Message: &fftypes.MessageInOut{ Message: fftypes.Message{ Header: fftypes.MessageHeader{ + ID: msgID, Type: fftypes.MessageTypeTransferPrivate, }, Hash: hash, @@ -933,6 +983,7 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -955,7 +1006,8 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { _, err := am.TransferTokens(context.Background(), "ns1", transfer, false) assert.NoError(t, err) - assert.Equal(t, *hash, *transfer.MessageHash) + assert.Equal(t, *msgID, *transfer.TokenTransfer.Message) + assert.Equal(t, *hash, *transfer.TokenTransfer.MessageHash) mpm.AssertExpectations(t) mim.AssertExpectations(t) @@ -1012,6 +1064,7 @@ func TestTransferTokensConfirm(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -1046,6 +1099,7 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() + msgID := fftypes.NewUUID() hash := fftypes.NewRandB32() transfer := &fftypes.TokenTransferInput{ TokenTransfer: fftypes.TokenTransfer{ @@ -1056,6 +1110,9 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { Pool: "pool1", Message: &fftypes.MessageInOut{ Message: fftypes.Message{ + Header: fftypes.MessageHeader{ + ID: msgID, + }, Hash: hash, }, InlineData: fftypes.InlineData{ @@ -1067,6 +1124,7 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -1102,7 +1160,8 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { _, err := am.TransferTokens(context.Background(), "ns1", transfer, true) assert.NoError(t, err) - assert.Equal(t, *hash, *transfer.MessageHash) + assert.Equal(t, *msgID, *transfer.TokenTransfer.Message) + assert.Equal(t, *hash, *transfer.TokenTransfer.MessageHash) mbm.AssertExpectations(t) mim.AssertExpectations(t) @@ -1125,6 +1184,7 @@ func TestTransferTokensByTypeSuccess(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) diff --git a/internal/broadcast/tokenpool.go b/internal/broadcast/tokenpool.go index da77ea813d..355bd4bb54 100644 --- a/internal/broadcast/tokenpool.go +++ b/internal/broadcast/tokenpool.go @@ -23,16 +23,16 @@ import ( ) func (bm *broadcastManager) BroadcastTokenPool(ctx context.Context, ns string, pool *fftypes.TokenPoolAnnouncement, waitConfirm bool) (msg *fftypes.Message, err error) { - if err := pool.Validate(ctx, false); err != nil { + if err := pool.Pool.Validate(ctx); err != nil { return nil, err } - if err := bm.data.VerifyNamespaceExists(ctx, pool.Namespace); err != nil { + if err := bm.data.VerifyNamespaceExists(ctx, pool.Pool.Namespace); err != nil { return nil, err } msg, err = bm.BroadcastDefinitionAsNode(ctx, pool, fftypes.SystemTagDefinePool, waitConfirm) if msg != nil { - pool.Message = msg.Header.ID + pool.Pool.Message = msg.Header.ID } return msg, err } diff --git a/internal/broadcast/tokenpool_test.go b/internal/broadcast/tokenpool_test.go index 6ad2e242b2..b1f45679ce 100644 --- a/internal/broadcast/tokenpool_test.go +++ b/internal/broadcast/tokenpool_test.go @@ -35,7 +35,7 @@ func TestBroadcastTokenPoolNSGetFail(t *testing.T) { mdm := bm.data.(*datamocks.Manager) pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ + Pool: &fftypes.TokenPool{ ID: fftypes.NewUUID(), Namespace: "ns1", Name: "mypool", @@ -43,7 +43,6 @@ func TestBroadcastTokenPoolNSGetFail(t *testing.T) { ProtocolID: "N1", Symbol: "COIN", }, - ProtocolTxID: "tx123", } mdm.On("VerifyNamespaceExists", mock.Anything, "ns1").Return(fmt.Errorf("pop")) @@ -61,7 +60,7 @@ func TestBroadcastTokenPoolInvalid(t *testing.T) { mdm := bm.data.(*datamocks.Manager) pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ + Pool: &fftypes.TokenPool{ ID: fftypes.NewUUID(), Namespace: "", Name: "", @@ -69,7 +68,6 @@ func TestBroadcastTokenPoolInvalid(t *testing.T) { ProtocolID: "N1", Symbol: "COIN", }, - ProtocolTxID: "tx123", } _, err := bm.BroadcastTokenPool(context.Background(), "ns1", pool, false) @@ -87,7 +85,7 @@ func TestBroadcastTokenPoolBroadcastFail(t *testing.T) { mim := bm.identity.(*identitymanagermocks.Manager) pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ + Pool: &fftypes.TokenPool{ ID: fftypes.NewUUID(), Namespace: "ns1", Name: "mypool", @@ -95,7 +93,6 @@ func TestBroadcastTokenPoolBroadcastFail(t *testing.T) { ProtocolID: "N1", Symbol: "COIN", }, - ProtocolTxID: "tx123", } mim.On("ResolveInputIdentity", mock.Anything, mock.Anything).Return(nil) @@ -119,7 +116,7 @@ func TestBroadcastTokenPoolOk(t *testing.T) { mim := bm.identity.(*identitymanagermocks.Manager) pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ + Pool: &fftypes.TokenPool{ ID: fftypes.NewUUID(), Namespace: "ns1", Name: "mypool", @@ -127,7 +124,6 @@ func TestBroadcastTokenPoolOk(t *testing.T) { ProtocolID: "N1", Symbol: "COIN", }, - ProtocolTxID: "tx123", } mim.On("ResolveInputIdentity", mock.Anything, mock.Anything).Return(nil) diff --git a/internal/database/sqlcommon/tokenpool_sql.go b/internal/database/sqlcommon/tokenpool_sql.go index 90d2bf1331..2bbac3dc84 100644 --- a/internal/database/sqlcommon/tokenpool_sql.go +++ b/internal/database/sqlcommon/tokenpool_sql.go @@ -38,6 +38,7 @@ var ( "connector", "symbol", "message_id", + "state", "created", "tx_type", "tx_id", @@ -90,6 +91,7 @@ func (s *SQLCommon) UpsertTokenPool(ctx context.Context, pool *fftypes.TokenPool Set("connector", pool.Connector). Set("symbol", pool.Symbol). Set("message_id", pool.Message). + Set("state", pool.State). Set("tx_type", pool.TX.Type). Set("tx_id", pool.TX.ID). Set("key", pool.Key). @@ -115,6 +117,7 @@ func (s *SQLCommon) UpsertTokenPool(ctx context.Context, pool *fftypes.TokenPool pool.Connector, pool.Symbol, pool.Message, + pool.State, pool.Created, pool.TX.Type, pool.TX.ID, @@ -143,6 +146,7 @@ func (s *SQLCommon) tokenPoolResult(ctx context.Context, row *sql.Rows) (*fftype &pool.Connector, &pool.Symbol, &pool.Message, + &pool.State, &pool.Created, &pool.TX.Type, &pool.TX.ID, @@ -186,8 +190,11 @@ func (s *SQLCommon) GetTokenPoolByID(ctx context.Context, id *fftypes.UUID) (mes return s.getTokenPoolPred(ctx, id.String(), sq.Eq{"id": id}) } -func (s *SQLCommon) GetTokenPoolByProtocolID(ctx context.Context, id string) (*fftypes.TokenPool, error) { - return s.getTokenPoolPred(ctx, id, sq.Eq{"protocol_id": id}) +func (s *SQLCommon) GetTokenPoolByProtocolID(ctx context.Context, connector, protocolID string) (*fftypes.TokenPool, error) { + return s.getTokenPoolPred(ctx, protocolID, sq.And{ + sq.Eq{"connector": connector}, + sq.Eq{"protocol_id": protocolID}, + }) } func (s *SQLCommon) GetTokenPools(ctx context.Context, filter database.Filter) (message []*fftypes.TokenPool, fr *database.FilterResult, err error) { diff --git a/internal/database/sqlcommon/tokenpool_sql_test.go b/internal/database/sqlcommon/tokenpool_sql_test.go index 82c41e5d8b..eb8ee668c7 100644 --- a/internal/database/sqlcommon/tokenpool_sql_test.go +++ b/internal/database/sqlcommon/tokenpool_sql_test.go @@ -47,6 +47,7 @@ func TestTokenPoolE2EWithDB(t *testing.T) { Connector: "erc1155", Symbol: "COIN", Message: fftypes.NewUUID(), + State: fftypes.TokenPoolStateConfirmed, TX: fftypes.TransactionRef{ Type: fftypes.TransactionTypeTokenPool, ID: fftypes.NewUUID(), @@ -80,7 +81,7 @@ func TestTokenPoolE2EWithDB(t *testing.T) { assert.Equal(t, string(poolJson), string(poolReadJson)) // Query back the token pool (by protocol ID) - poolRead, err = s.GetTokenPoolByProtocolID(ctx, pool.ProtocolID) + poolRead, err = s.GetTokenPoolByProtocolID(ctx, pool.Connector, pool.ProtocolID) assert.NoError(t, err) assert.NotNil(t, poolRead) poolReadJson, _ = json.Marshal(&poolRead) diff --git a/internal/database/sqlcommon/tokentransfer_sql.go b/internal/database/sqlcommon/tokentransfer_sql.go index ee8f9a1349..2b9ae33217 100644 --- a/internal/database/sqlcommon/tokentransfer_sql.go +++ b/internal/database/sqlcommon/tokentransfer_sql.go @@ -40,6 +40,7 @@ var ( "to_key", "amount", "protocol_id", + "message_id", "message_hash", "tx_type", "tx_id", @@ -52,6 +53,7 @@ var ( "from": "from_key", "to": "to_key", "protocolid": "protocol_id", + "message": "message_id", "messagehash": "message_hash", "transaction.type": "tx_type", "transaction.id": "tx_id", @@ -89,6 +91,7 @@ func (s *SQLCommon) UpsertTokenTransfer(ctx context.Context, transfer *fftypes.T Set("from_key", transfer.From). Set("to_key", transfer.To). Set("amount", transfer.Amount). + Set("message_id", transfer.Message). Set("message_hash", transfer.MessageHash). Set("tx_type", transfer.TX.Type). Set("tx_id", transfer.TX.ID). @@ -116,6 +119,7 @@ func (s *SQLCommon) UpsertTokenTransfer(ctx context.Context, transfer *fftypes.T transfer.To, transfer.Amount, transfer.ProtocolID, + transfer.Message, transfer.MessageHash, transfer.TX.Type, transfer.TX.ID, @@ -146,6 +150,7 @@ func (s *SQLCommon) tokenTransferResult(ctx context.Context, row *sql.Rows) (*ff &transfer.To, &transfer.Amount, &transfer.ProtocolID, + &transfer.Message, &transfer.MessageHash, &transfer.TX.Type, &transfer.TX.ID, @@ -185,6 +190,13 @@ func (s *SQLCommon) GetTokenTransfer(ctx context.Context, localID *fftypes.UUID) return s.getTokenTransferPred(ctx, localID.String(), sq.Eq{"local_id": localID}) } +func (s *SQLCommon) GetTokenTransferByProtocolID(ctx context.Context, connector, protocolID string) (*fftypes.TokenTransfer, error) { + return s.getTokenTransferPred(ctx, protocolID, sq.And{ + sq.Eq{"connector": connector}, + sq.Eq{"protocol_id": protocolID}, + }) +} + func (s *SQLCommon) GetTokenTransfers(ctx context.Context, filter database.Filter) (message []*fftypes.TokenTransfer, fr *database.FilterResult, err error) { query, fop, fi, err := s.filterSelect(ctx, "", sq.Select(tokenTransferColumns...).From("tokentransfer"), filter, tokenTransferFilterFieldMap, []interface{}{"seq"}) if err != nil { diff --git a/internal/database/sqlcommon/tokentransfer_sql_test.go b/internal/database/sqlcommon/tokentransfer_sql_test.go index f3b7c60824..54fb3ea4af 100644 --- a/internal/database/sqlcommon/tokentransfer_sql_test.go +++ b/internal/database/sqlcommon/tokentransfer_sql_test.go @@ -45,6 +45,7 @@ func TestTokenTransferE2EWithDB(t *testing.T) { From: "0x01", To: "0x02", ProtocolID: "12345", + Message: fftypes.NewUUID(), MessageHash: fftypes.NewRandB32(), TX: fftypes.TransactionRef{ Type: fftypes.TransactionTypeTokenTransfer, @@ -71,6 +72,13 @@ func TestTokenTransferE2EWithDB(t *testing.T) { transferReadJson, _ := json.Marshal(&transferRead) assert.Equal(t, string(transferJson), string(transferReadJson)) + // Query back the token transfer (by protocol ID) + transferRead, err = s.GetTokenTransferByProtocolID(ctx, transfer.Connector, transfer.ProtocolID) + assert.NoError(t, err) + assert.NotNil(t, transferRead) + transferReadJson, _ = json.Marshal(&transferRead) + assert.Equal(t, string(transferJson), string(transferReadJson)) + // Query back the token transfer (by query filter) fb := database.TokenTransferQueryFactory.NewFilter(ctx) filter := fb.And( diff --git a/internal/events/aggregator.go b/internal/events/aggregator.go index 12da8eb529..d7ab6bc2a6 100644 --- a/internal/events/aggregator.go +++ b/internal/events/aggregator.go @@ -416,11 +416,14 @@ func (ag *aggregator) attemptMessageDispatch(ctx context.Context, msg *fftypes.M if msg.Header.Type == fftypes.MessageTypeTransferBroadcast || msg.Header.Type == fftypes.MessageTypeTransferPrivate { fb := database.TokenTransferQueryFactory.NewFilter(ctx) filter := fb.And( - fb.Eq("messagehash", msg.Hash), + fb.Eq("message", msg.Header.ID), ) if transfers, _, err := ag.database.GetTokenTransfers(ctx, filter); err != nil || len(transfers) == 0 { - log.L(ctx).Debugf("Transfer for message %s not yet available", msg.Hash) + log.L(ctx).Debugf("Transfer for message %s not yet available", msg.Header.ID) return false, err + } else if !msg.Hash.Equals(transfers[0].MessageHash) { + log.L(ctx).Errorf("Message hash %s does not match hash recorded in transfer: %s", msg.Hash, transfers[0].MessageHash) + return false, nil } } @@ -431,20 +434,23 @@ func (ag *aggregator) attemptMessageDispatch(ctx context.Context, msg *fftypes.M case msg.Header.Namespace == fftypes.SystemNamespace: // We handle system events in-line on the aggregator, as it would be confusing for apps to be // dispatched subsequent events before we have processed the system events they depend on. - if valid, err = ag.syshandlers.HandleSystemBroadcast(ctx, msg, data); err != nil { - // Should only return errors that are retryable + var action syshandlers.SystemBroadcastAction + action, err = ag.syshandlers.HandleSystemBroadcast(ctx, msg, data) + if action == syshandlers.ActionRetry || action == syshandlers.ActionWait { return false, err } + valid = action == syshandlers.ActionConfirm + case msg.Header.Type == fftypes.MessageTypeGroupInit: - // Already handled as part of resolving the context. - valid = true - eventType = fftypes.EventTypeGroupConfirmed + // Already handled as part of resolving the context - do nothing. + case len(msg.Data) > 0: valid, err = ag.data.ValidateAll(ctx, data) if err != nil { return false, err } } + // This message is now confirmed state := fftypes.MessageStateConfirmed if !valid { diff --git a/internal/events/aggregator_test.go b/internal/events/aggregator_test.go index dec44a69f9..dbccb518b1 100644 --- a/internal/events/aggregator_test.go +++ b/internal/events/aggregator_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/hyperledger/firefly/internal/config" + "github.com/hyperledger/firefly/internal/syshandlers" "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/datamocks" "github.com/hyperledger/firefly/mocks/syshandlersmocks" @@ -932,12 +933,43 @@ func TestAttemptMessageDispatchGetTransfersFail(t *testing.T) { mdi.AssertExpectations(t) } +func TestAttemptMessageDispatchTransferMismatch(t *testing.T) { + ag, cancel := newTestAggregator() + defer cancel() + + msg := &fftypes.Message{ + Header: fftypes.MessageHeader{ + ID: fftypes.NewUUID(), + Type: fftypes.MessageTypeTransferBroadcast, + }, + } + msg.Hash = msg.Header.Hash() + + transfers := []*fftypes.TokenTransfer{{ + Message: msg.Header.ID, + MessageHash: fftypes.NewRandB32(), + }} + + mdm := ag.data.(*datamocks.Manager) + mdm.On("GetMessageData", ag.ctx, mock.Anything, true).Return([]*fftypes.Data{}, true, nil) + + mdi := ag.database.(*databasemocks.Plugin) + mdi.On("GetTokenTransfers", ag.ctx, mock.Anything).Return(transfers, nil, nil) + + dispatched, err := ag.attemptMessageDispatch(ag.ctx, msg) + assert.NoError(t, err) + assert.False(t, dispatched) + + mdm.AssertExpectations(t) + mdi.AssertExpectations(t) +} + func TestAttemptMessageDispatchFailValidateBadSystem(t *testing.T) { ag, cancel := newTestAggregator() defer cancel() msh := ag.syshandlers.(*syshandlersmocks.SystemHandlers) - msh.On("HandleSystemBroadcast", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) + msh.On("HandleSystemBroadcast", mock.Anything, mock.Anything, mock.Anything).Return(syshandlers.ActionReject, nil) mdm := ag.data.(*datamocks.Manager) mdm.On("GetMessageData", ag.ctx, mock.Anything, true).Return([]*fftypes.Data{}, true, nil) @@ -980,7 +1012,7 @@ func TestAttemptMessageDispatchFailValidateSystemFail(t *testing.T) { defer cancel() msh := ag.syshandlers.(*syshandlersmocks.SystemHandlers) - msh.On("HandleSystemBroadcast", mock.Anything, mock.Anything, mock.Anything).Return(false, fmt.Errorf("pop")) + msh.On("HandleSystemBroadcast", mock.Anything, mock.Anything, mock.Anything).Return(syshandlers.ActionRetry, fmt.Errorf("pop")) mdm := ag.data.(*datamocks.Manager) mdm.On("GetMessageData", ag.ctx, mock.Anything, true).Return([]*fftypes.Data{}, true, nil) diff --git a/internal/events/event_manager.go b/internal/events/event_manager.go index 30f04c88be..b0025b9d62 100644 --- a/internal/events/event_manager.go +++ b/internal/events/event_manager.go @@ -22,6 +22,7 @@ import ( "encoding/json" "strconv" + "github.com/hyperledger/firefly/internal/assets" "github.com/hyperledger/firefly/internal/broadcast" "github.com/hyperledger/firefly/internal/config" "github.com/hyperledger/firefly/internal/data" @@ -65,7 +66,8 @@ type EventManager interface { MessageReceived(dx dataexchange.Plugin, peerID string, data []byte) error // Bound token callbacks - TokensTransferred(tk tokens.Plugin, poolProtocolID string, transfer *fftypes.TokenTransfer, protocolTxID string, additionalInfo fftypes.JSONObject) error + TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error + TokensTransferred(ti tokens.Plugin, poolProtocolID string, transfer *fftypes.TokenTransfer, protocolTxID string, additionalInfo fftypes.JSONObject) error // Internal events sysmessaging.SystemEvents @@ -84,6 +86,7 @@ type eventManager struct { aggregator *aggregator broadcast broadcast.Manager messaging privatemessaging.Manager + assets assets.Manager newEventNotifier *eventNotifier newPinNotifier *eventNotifier opCorrelationRetries int @@ -91,8 +94,8 @@ type eventManager struct { internalEvents *system.Events } -func NewEventManager(ctx context.Context, pi publicstorage.Plugin, di database.Plugin, im identity.Manager, sh syshandlers.SystemHandlers, dm data.Manager, bm broadcast.Manager, pm privatemessaging.Manager) (EventManager, error) { - if pi == nil || di == nil || im == nil || dm == nil || bm == nil || pm == nil { +func NewEventManager(ctx context.Context, pi publicstorage.Plugin, di database.Plugin, im identity.Manager, sh syshandlers.SystemHandlers, dm data.Manager, bm broadcast.Manager, pm privatemessaging.Manager, am assets.Manager) (EventManager, error) { + if pi == nil || di == nil || im == nil || dm == nil || bm == nil || pm == nil || am == nil { return nil, i18n.NewError(ctx, i18n.MsgInitializationNilDepError) } newPinNotifier := newEventNotifier(ctx, "pins") @@ -106,6 +109,7 @@ func NewEventManager(ctx context.Context, pi publicstorage.Plugin, di database.P data: dm, broadcast: bm, messaging: pm, + assets: am, retry: retry.Retry{ InitialDelay: config.GetDuration(config.EventAggregatorRetryInitDelay), MaximumDelay: config.GetDuration(config.EventAggregatorRetryMaxDelay), diff --git a/internal/events/event_manager_test.go b/internal/events/event_manager_test.go index 9d512b1300..a7dd60b0e6 100644 --- a/internal/events/event_manager_test.go +++ b/internal/events/event_manager_test.go @@ -23,6 +23,7 @@ import ( "github.com/hyperledger/firefly/internal/config" "github.com/hyperledger/firefly/internal/events/system" + "github.com/hyperledger/firefly/mocks/assetmocks" "github.com/hyperledger/firefly/mocks/broadcastmocks" "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/datamocks" @@ -47,8 +48,9 @@ func newTestEventManager(t *testing.T) (*eventManager, func()) { msh := &syshandlersmocks.SystemHandlers{} mbm := &broadcastmocks.Manager{} mpm := &privatemessagingmocks.Manager{} + mam := &assetmocks.Manager{} met.On("Name").Return("ut").Maybe() - emi, err := NewEventManager(ctx, mpi, mdi, mim, msh, mdm, mbm, mpm) + emi, err := NewEventManager(ctx, mpi, mdi, mim, msh, mdm, mbm, mpm, mam) em := emi.(*eventManager) rag := mdi.On("RunAsGroup", em.ctx, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { @@ -78,7 +80,7 @@ func TestStartStop(t *testing.T) { } func TestStartStopBadDependencies(t *testing.T) { - _, err := NewEventManager(context.Background(), nil, nil, nil, nil, nil, nil, nil) + _, err := NewEventManager(context.Background(), nil, nil, nil, nil, nil, nil, nil, nil) assert.Regexp(t, "FF10128", err) } @@ -93,7 +95,8 @@ func TestStartStopBadTransports(t *testing.T) { msh := &syshandlersmocks.SystemHandlers{} mbm := &broadcastmocks.Manager{} mpm := &privatemessagingmocks.Manager{} - _, err := NewEventManager(context.Background(), mpi, mdi, mim, msh, mdm, mbm, mpm) + mam := &assetmocks.Manager{} + _, err := NewEventManager(context.Background(), mpi, mdi, mim, msh, mdm, mbm, mpm, mam) assert.Regexp(t, "FF10172", err) } diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go new file mode 100644 index 0000000000..33d2d5b05a --- /dev/null +++ b/internal/events/token_pool_created.go @@ -0,0 +1,197 @@ +// 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 events + +import ( + "context" + + "github.com/hyperledger/firefly/internal/log" + "github.com/hyperledger/firefly/internal/txcommon" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/hyperledger/firefly/pkg/tokens" +) + +func addPoolDetailsFromPlugin(ffPool *fftypes.TokenPool, pluginPool *tokens.TokenPool) { + ffPool.Type = pluginPool.Type + ffPool.ProtocolID = pluginPool.ProtocolID + ffPool.Key = pluginPool.Key + ffPool.Connector = pluginPool.Connector + ffPool.Standard = pluginPool.Standard + if pluginPool.TransactionID != nil { + ffPool.TX = fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: pluginPool.TransactionID, + } + } +} + +func poolTransaction(pool *fftypes.TokenPool, status fftypes.OpStatus, protocolTxID string, additionalInfo fftypes.JSONObject) *fftypes.Transaction { + return &fftypes.Transaction{ + ID: pool.TX.ID, + Status: status, + Subject: fftypes.TransactionSubject{ + Namespace: pool.Namespace, + Type: pool.TX.Type, + Signer: pool.Key, + Reference: pool.ID, + }, + ProtocolID: protocolTxID, + Info: additionalInfo, + } +} + +func (em *eventManager) confirmPool(ctx context.Context, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { + tx := poolTransaction(pool, fftypes.OpStatusSucceeded, protocolTxID, additionalInfo) + if valid, err := em.txhelper.PersistTransaction(ctx, tx); !valid || err != nil { + return err + } + pool.State = fftypes.TokenPoolStateConfirmed + if err := em.database.UpsertTokenPool(ctx, pool); err != nil { + return err + } + log.L(ctx).Infof("Token pool confirmed id=%s author=%s", pool.ID, pool.Key) + event := fftypes.NewEvent(fftypes.EventTypePoolConfirmed, pool.Namespace, pool.ID) + return em.database.InsertEvent(ctx, event) +} + +func (em *eventManager) findTokenPoolCreateOp(ctx context.Context, tx *fftypes.UUID) (*fftypes.Operation, error) { + // Find a matching operation within this transaction + fb := database.OperationQueryFactory.NewFilter(ctx) + filter := fb.And( + fb.Eq("tx", tx), + fb.Eq("type", fftypes.OpTypeTokenCreatePool), + ) + if operations, _, err := em.database.GetOperations(ctx, filter); err != nil { + return nil, err + } else if len(operations) > 0 { + return operations[0], nil + } + return nil, nil +} + +func (em *eventManager) shouldConfirm(ctx context.Context, pool *tokens.TokenPool) (existingPool *fftypes.TokenPool, err error) { + if existingPool, err = em.database.GetTokenPoolByProtocolID(ctx, pool.Connector, pool.ProtocolID); err != nil || existingPool == nil { + return existingPool, err + } + addPoolDetailsFromPlugin(existingPool, pool) + + if existingPool.State == fftypes.TokenPoolStateUnknown { + // Unknown pool state - should only happen on first run after database migration + // Activate the pool, then immediately confirm + // TODO: can this state eventually be removed? + tx, err := em.database.GetTransactionByID(ctx, existingPool.TX.ID) + if err != nil { + return nil, err + } + if err = em.assets.ActivateTokenPool(ctx, existingPool, tx); err != nil { + log.L(ctx).Errorf("Failed to activate token pool '%s': %s", existingPool.ID, err) + return nil, err + } + } + return existingPool, nil +} + +func (em *eventManager) shouldAnnounce(ctx context.Context, ti tokens.Plugin, pool *tokens.TokenPool) (announcePool *fftypes.TokenPool, err error) { + if pool.TransactionID == nil { + return nil, nil + } + + op, err := em.findTokenPoolCreateOp(ctx, pool.TransactionID) + if err != nil { + return nil, err + } else if op == nil { + return nil, nil + } + + announcePool = &fftypes.TokenPool{} + if err = txcommon.RetrieveTokenPoolCreateInputs(ctx, op, announcePool); err != nil { + log.L(ctx).Errorf("Error loading pool info for transaction '%s' (%s) - ignoring: %v", pool.TransactionID, err, op.Input) + return nil, nil + } + addPoolDetailsFromPlugin(announcePool, pool) + + nextOp := fftypes.NewTXOperation( + ti, + op.Namespace, + op.Transaction, + "", + fftypes.OpTypeTokenAnnouncePool, + fftypes.OpStatusPending) + return announcePool, em.database.UpsertOperation(ctx, nextOp, false) +} + +// It is expected that this method might be invoked twice for each pool, depending on the behavior of the connector. +// It will be at least invoked on the submitter when the pool is first created, to trigger the submitter to announce it. +// It will be invoked on every node (including the submitter) after the pool is announced+activated, to trigger confirmation of the pool. +// When received in any other scenario, it should be ignored. +func (em *eventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) (err error) { + var batchID *fftypes.UUID + var announcePool *fftypes.TokenPool + + err = em.retry.Do(em.ctx, "persist token pool transaction", func(attempt int) (bool, error) { + err := em.database.RunAsGroup(em.ctx, func(ctx context.Context) error { + // See if this is a confirmation of an unconfirmed pool + if existingPool, err := em.shouldConfirm(ctx, pool); err != nil { + return err + } else if existingPool != nil { + if existingPool.State == fftypes.TokenPoolStateConfirmed { + return nil // already confirmed + } + if msg, err := em.database.GetMessageByID(ctx, existingPool.Message); err != nil { + return err + } else if msg != nil { + batchID = msg.BatchID // trigger rewind after completion of database transaction + } + return em.confirmPool(ctx, existingPool, protocolTxID, additionalInfo) + } + + // See if this pool was submitted locally and needs to be announced + if announcePool, err = em.shouldAnnounce(ctx, ti, pool); err != nil { + return err + } else if announcePool != nil { + return nil // trigger announce after completion of database transaction + } + + // Otherwise this event can be ignored + log.L(ctx).Debugf("Ignoring token pool transaction '%s' - pool %s is not active", protocolTxID, pool.ProtocolID) + return nil + }) + return err != nil, err + }) + + if err == nil { + // Initiate a rewind if a batch was potentially completed by the arrival of this transaction + if batchID != nil { + log.L(em.ctx).Infof("Batch '%s' contains reference to received pool '%s'", batchID, pool.ProtocolID) + em.aggregator.offchainBatches <- batchID + } + + // Announce the details of the new token pool and the transaction object + // Other nodes will pass these details to their own token connector for validation/activation of the pool + if announcePool != nil { + broadcast := &fftypes.TokenPoolAnnouncement{ + Pool: announcePool, + TX: poolTransaction(announcePool, fftypes.OpStatusPending, protocolTxID, additionalInfo), + } + log.L(em.ctx).Infof("Announcing token pool id=%s author=%s", announcePool.ID, pool.Key) + _, err = em.broadcast.BroadcastTokenPool(em.ctx, announcePool.Namespace, broadcast, false) + } + } + + return err +} diff --git a/internal/events/token_pool_created_test.go b/internal/events/token_pool_created_test.go new file mode 100644 index 0000000000..f1eef19b13 --- /dev/null +++ b/internal/events/token_pool_created_test.go @@ -0,0 +1,404 @@ +// 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 events + +import ( + "fmt" + "testing" + + "github.com/hyperledger/firefly/mocks/assetmocks" + "github.com/hyperledger/firefly/mocks/broadcastmocks" + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/tokenmocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/hyperledger/firefly/pkg/tokens" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestTokenPoolCreatedIgnore(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mti := &tokenmocks.Plugin{} + + txID := fftypes.NewUUID() + operations := []*fftypes.Operation{} + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, + Connector: "erc1155", + } + + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil, nil) + mdi.On("GetOperations", em.ctx, mock.Anything).Return(operations, nil, nil) + + info := fftypes.JSONObject{"some": "info"} + err := em.TokenPoolCreated(mti, pool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestTokenPoolCreatedIgnoreNoTX(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mti := &tokenmocks.Plugin{} + + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: nil, + Connector: "erc1155", + } + + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil, nil) + + info := fftypes.JSONObject{"some": "info"} + err := em.TokenPoolCreated(mti, pool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestTokenPoolCreatedConfirm(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mti := &tokenmocks.Plugin{} + + txID := fftypes.NewUUID() + chainPool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + Connector: "erc1155", + } + storedPool := &fftypes.TokenPool{ + Namespace: "ns1", + ID: fftypes.NewUUID(), + Key: chainPool.Key, + State: fftypes.TokenPoolStatePending, + Message: fftypes.NewUUID(), + TX: fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: txID, + }, + } + storedTX := &fftypes.Transaction{ + Subject: fftypes.TransactionSubject{ + Namespace: "ns1", + Reference: storedPool.ID, + Signer: storedPool.Key, + Type: fftypes.TransactionTypeTokenPool, + }, + } + storedMessage := &fftypes.Message{ + BatchID: fftypes.NewUUID(), + } + + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(storedPool, nil).Times(2) + mdi.On("GetTransactionByID", em.ctx, txID).Return(storedTX, nil) + mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return *tx.Subject.Reference == *storedTX.Subject.Reference + }), false).Return(nil) + mdi.On("UpsertTokenPool", em.ctx, storedPool).Return(nil) + mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { + return e.Type == fftypes.EventTypePoolConfirmed && *e.Reference == *storedPool.ID + })).Return(nil) + mdi.On("GetMessageByID", em.ctx, storedPool.Message).Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetMessageByID", em.ctx, storedPool.Message).Return(storedMessage, nil).Once() + + info := fftypes.JSONObject{"some": "info"} + err := em.TokenPoolCreated(mti, chainPool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestTokenPoolCreatedAlreadyConfirmed(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mti := &tokenmocks.Plugin{} + + txID := fftypes.NewUUID() + chainPool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + Connector: "erc1155", + } + storedPool := &fftypes.TokenPool{ + Namespace: "ns1", + ID: fftypes.NewUUID(), + Key: chainPool.Key, + State: fftypes.TokenPoolStateConfirmed, + TX: fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: txID, + }, + } + + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(storedPool, nil) + + info := fftypes.JSONObject{"some": "info"} + err := em.TokenPoolCreated(mti, chainPool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestTokenPoolCreatedMigrate(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mam := em.assets.(*assetmocks.Manager) + mti := &tokenmocks.Plugin{} + + txID := fftypes.NewUUID() + chainPool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + Connector: "magic-tokens", + } + storedPool := &fftypes.TokenPool{ + Namespace: "ns1", + ID: fftypes.NewUUID(), + Key: chainPool.Key, + State: fftypes.TokenPoolStateUnknown, + TX: fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: txID, + }, + } + storedTX := &fftypes.Transaction{ + Subject: fftypes.TransactionSubject{ + Namespace: "ns1", + Reference: storedPool.ID, + Signer: storedPool.Key, + Type: fftypes.TransactionTypeTokenPool, + }, + } + storedMessage := &fftypes.Message{ + BatchID: fftypes.NewUUID(), + } + + mdi.On("GetTokenPoolByProtocolID", em.ctx, "magic-tokens", "123").Return(storedPool, nil).Times(3) + mdi.On("GetTransactionByID", em.ctx, storedPool.TX.ID).Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetTransactionByID", em.ctx, storedPool.TX.ID).Return(storedTX, nil).Times(3) + mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return *tx.Subject.Reference == *storedTX.Subject.Reference + }), false).Return(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 + })).Return(nil).Once() + mam.On("ActivateTokenPool", em.ctx, storedPool, storedTX).Return(fmt.Errorf("pop")).Once() + mam.On("ActivateTokenPool", em.ctx, storedPool, storedTX).Return(nil).Once() + mdi.On("GetMessageByID", em.ctx, storedPool.Message).Return(storedMessage, nil) + + info := fftypes.JSONObject{"some": "info"} + err := em.TokenPoolCreated(mti, chainPool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mam.AssertExpectations(t) +} + +func TestConfirmPoolTxFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + + txID := fftypes.NewUUID() + storedPool := &fftypes.TokenPool{ + Namespace: "ns1", + ID: fftypes.NewUUID(), + Key: "0x0", + State: fftypes.TokenPoolStatePending, + TX: fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: txID, + }, + } + + mdi.On("GetTransactionByID", em.ctx, txID).Return(nil, fmt.Errorf("pop")) + + info := fftypes.JSONObject{"some": "info"} + err := em.confirmPool(em.ctx, storedPool, "tx1", info) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestConfirmPoolUpsertFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + + txID := fftypes.NewUUID() + storedPool := &fftypes.TokenPool{ + Namespace: "ns1", + ID: fftypes.NewUUID(), + Key: "0x0", + State: fftypes.TokenPoolStatePending, + TX: fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: txID, + }, + } + storedTX := &fftypes.Transaction{ + Subject: fftypes.TransactionSubject{ + Namespace: "ns1", + Reference: storedPool.ID, + Signer: storedPool.Key, + Type: fftypes.TransactionTypeTokenPool, + }, + } + + mdi.On("GetTransactionByID", em.ctx, txID).Return(storedTX, nil) + mdi.On("UpsertTransaction", em.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return *tx.Subject.Reference == *storedTX.Subject.Reference + }), false).Return(nil) + mdi.On("UpsertTokenPool", em.ctx, storedPool).Return(fmt.Errorf("pop")) + + info := fftypes.JSONObject{"some": "info"} + err := em.confirmPool(em.ctx, storedPool, "tx1", info) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestTokenPoolCreatedAnnounce(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mti := &tokenmocks.Plugin{} + mbm := em.broadcast.(*broadcastmocks.Manager) + + poolID := fftypes.NewUUID() + txID := fftypes.NewUUID() + operations := []*fftypes.Operation{ + { + ID: fftypes.NewUUID(), + Input: fftypes.JSONObject{ + "id": poolID.String(), + "namespace": "test-ns", + "name": "my-pool", + }, + }, + } + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, + Connector: "erc1155", + } + + mti.On("Name").Return("mock-tokens") + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil).Times(2) + mdi.On("GetOperations", em.ctx, mock.Anything).Return(nil, nil, fmt.Errorf("pop")).Once() + mdi.On("GetOperations", em.ctx, mock.Anything).Return(operations, nil, nil).Once() + mdi.On("UpsertOperation", em.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Type == fftypes.OpTypeTokenAnnouncePool + }), false).Return(nil) + mbm.On("BroadcastTokenPool", em.ctx, "test-ns", mock.MatchedBy(func(pool *fftypes.TokenPoolAnnouncement) bool { + return pool.Pool.Namespace == "test-ns" && pool.Pool.Name == "my-pool" && *pool.Pool.ID == *poolID + }), false).Return(nil, nil) + + info := fftypes.JSONObject{"some": "info"} + err := em.TokenPoolCreated(mti, pool, "tx1", info) + assert.NoError(t, err) + + mti.AssertExpectations(t) + mdi.AssertExpectations(t) + mbm.AssertExpectations(t) +} + +func TestTokenPoolCreatedAnnounceBadOpInputID(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mti := &tokenmocks.Plugin{} + + txID := fftypes.NewUUID() + operations := []*fftypes.Operation{ + { + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenCreatePool, + Input: fftypes.JSONObject{}, + }, + } + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, + Connector: "erc1155", + } + + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil) + mdi.On("GetOperations", em.ctx, mock.Anything).Return(operations, nil, nil) + + info := fftypes.JSONObject{"some": "info"} + err := em.TokenPoolCreated(mti, pool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestTokenPoolCreatedAnnounceBadOpInputNS(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + mdi := em.database.(*databasemocks.Plugin) + mti := &tokenmocks.Plugin{} + + txID := fftypes.NewUUID() + operations := []*fftypes.Operation{ + { + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenCreatePool, + Input: fftypes.JSONObject{ + "id": fftypes.NewUUID().String(), + }, + }, + } + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, + Connector: "erc1155", + } + + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil) + mdi.On("GetOperations", em.ctx, mock.Anything).Return(operations, nil, nil) + + info := fftypes.JSONObject{"some": "info"} + err := em.TokenPoolCreated(mti, pool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} diff --git a/internal/events/tokens_transferred.go b/internal/events/tokens_transferred.go index acb651b3e4..d4f1cde2d9 100644 --- a/internal/events/tokens_transferred.go +++ b/internal/events/tokens_transferred.go @@ -67,27 +67,21 @@ func (em *eventManager) persistTokenTransaction(ctx context.Context, ns string, return em.txhelper.PersistTransaction(ctx, transaction) } -func (em *eventManager) getMessageForTransfer(ctx context.Context, transfer *fftypes.TokenTransfer) (*fftypes.Message, error) { - var messages []*fftypes.Message - fb := database.MessageQueryFactory.NewFilter(ctx) - filter := fb.And( - fb.Eq("confirmed", nil), - fb.Eq("hash", transfer.MessageHash), - ) - messages, _, err := em.database.GetMessages(ctx, filter) - if err != nil || len(messages) == 0 { - return nil, err - } - return messages[0], nil -} - -func (em *eventManager) TokensTransferred(tk tokens.Plugin, poolProtocolID string, transfer *fftypes.TokenTransfer, protocolTxID string, additionalInfo fftypes.JSONObject) error { +func (em *eventManager) TokensTransferred(ti tokens.Plugin, poolProtocolID string, transfer *fftypes.TokenTransfer, protocolTxID string, additionalInfo fftypes.JSONObject) error { var batchID *fftypes.UUID err := em.retry.Do(em.ctx, "persist token transfer", func(attempt int) (bool, error) { err := em.database.RunAsGroup(em.ctx, func(ctx context.Context) error { + // Check that transfer has not already been recorded + if existing, err := em.database.GetTokenTransferByProtocolID(ctx, transfer.Connector, transfer.ProtocolID); err != nil { + return err + } else if existing != nil { + log.L(ctx).Warnf("Token transfer '%s' has already been recorded - ignoring", transfer.ProtocolID) + return nil + } + // Check that this is from a known pool - pool, err := em.database.GetTokenPoolByProtocolID(ctx, poolProtocolID) + pool, err := em.database.GetTokenPoolByProtocolID(ctx, transfer.Connector, poolProtocolID) if err != nil { return err } @@ -119,8 +113,8 @@ func (em *eventManager) TokensTransferred(tk tokens.Plugin, poolProtocolID strin } log.L(ctx).Infof("Token transfer recorded id=%s author=%s", transfer.ProtocolID, transfer.Key) - if transfer.MessageHash != nil { - msg, err := em.getMessageForTransfer(ctx, transfer) + if transfer.Message != nil { + msg, err := em.database.GetMessageByID(ctx, transfer.Message) if err != nil { return err } @@ -144,12 +138,10 @@ func (em *eventManager) TokensTransferred(tk tokens.Plugin, poolProtocolID strin return err != nil, err // retry indefinitely (until context closes) }) - if err == nil { - // Initiate a rewind if a batch was potentially completed by the arrival of this transfer - if batchID != nil { - log.L(em.ctx).Infof("Batch '%s' contains reference to received transfer. Transfer='%s' Message='%s'", batchID, transfer.ProtocolID, transfer.MessageHash) - em.aggregator.offchainBatches <- batchID - } + // Initiate a rewind if a batch was potentially completed by the arrival of this transfer + if err == nil && batchID != nil { + log.L(em.ctx).Infof("Batch '%s' contains reference to received transfer. Transfer='%s' Message='%s'", batchID, transfer.ProtocolID, transfer.Message) + em.aggregator.offchainBatches <- batchID } return err diff --git a/internal/events/tokens_transferred_test.go b/internal/events/tokens_transferred_test.go index 597222a608..c5844a5f12 100644 --- a/internal/events/tokens_transferred_test.go +++ b/internal/events/tokens_transferred_test.go @@ -42,14 +42,17 @@ func TestTokensTransferredSucceedWithRetries(t *testing.T) { Key: "0x12345", From: "0x1", To: "0x2", + ProtocolID: "123", Amount: *fftypes.NewBigInt(1), } pool := &fftypes.TokenPool{ Namespace: "ns1", } - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(nil, fmt.Errorf("pop")).Once() - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(pool, nil).Times(3) + mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil).Times(4) + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil).Times(3) mdi.On("UpsertTokenTransfer", em.ctx, transfer).Return(fmt.Errorf("pop")).Once() mdi.On("UpsertTokenTransfer", em.ctx, transfer).Return(nil).Times(2) mdi.On("UpdateTokenBalances", em.ctx, transfer).Return(fmt.Errorf("pop")).Once() @@ -66,6 +69,34 @@ func TestTokensTransferredSucceedWithRetries(t *testing.T) { mti.AssertExpectations(t) } +func TestTokensTransferredIgnoreExisting(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mdi := em.database.(*databasemocks.Plugin) + mti := &tokenmocks.Plugin{} + + transfer := &fftypes.TokenTransfer{ + Type: fftypes.TokenTransferTypeTransfer, + TokenIndex: "0", + Connector: "erc1155", + Key: "0x12345", + From: "0x1", + To: "0x2", + ProtocolID: "123", + Amount: *fftypes.NewBigInt(1), + } + + mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(&fftypes.TokenTransfer{}, nil) + + info := fftypes.JSONObject{"some": "info"} + err := em.TokensTransferred(mti, "F1", transfer, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mti.AssertExpectations(t) +} + func TestTokensTransferredWithTransactionRetries(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() @@ -80,6 +111,7 @@ func TestTokensTransferredWithTransactionRetries(t *testing.T) { Key: "0x12345", From: "0x1", To: "0x2", + ProtocolID: "123", Amount: *fftypes.NewBigInt(1), TX: fftypes.TransactionRef{ ID: fftypes.NewUUID(), @@ -100,7 +132,8 @@ func TestTokensTransferredWithTransactionRetries(t *testing.T) { }, }} - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(pool, nil).Times(3) + mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil).Times(3) + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil).Times(3) mdi.On("GetOperations", em.ctx, mock.Anything).Return(nil, nil, fmt.Errorf("pop")).Once() mdi.On("GetOperations", em.ctx, mock.Anything).Return(operationsBad, nil, nil).Once() mdi.On("GetOperations", em.ctx, mock.Anything).Return(operationsGood, nil, nil).Once() @@ -118,7 +151,7 @@ func TestTokensTransferredWithTransactionRetries(t *testing.T) { mti.AssertExpectations(t) } -func TestTokensTransferredAddBalanceIgnore(t *testing.T) { +func TestTokensTransferredBadPool(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() @@ -132,10 +165,12 @@ func TestTokensTransferredAddBalanceIgnore(t *testing.T) { Key: "0x12345", From: "0x1", To: "0x2", + ProtocolID: "123", Amount: *fftypes.NewBigInt(1), } - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(nil, nil) + mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil) + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(nil, nil) info := fftypes.JSONObject{"some": "info"} err := em.TokensTransferred(mti, "F1", transfer, "tx1", info) @@ -153,26 +188,29 @@ func TestTokensTransferredWithMessageReceived(t *testing.T) { mti := &tokenmocks.Plugin{} transfer := &fftypes.TokenTransfer{ - Type: fftypes.TokenTransferTypeTransfer, - TokenIndex: "0", - Key: "0x12345", - From: "0x1", - To: "0x2", - MessageHash: fftypes.NewRandB32(), - Amount: *fftypes.NewBigInt(1), + Type: fftypes.TokenTransferTypeTransfer, + TokenIndex: "0", + Connector: "erc1155", + Key: "0x12345", + From: "0x1", + To: "0x2", + ProtocolID: "123", + Message: fftypes.NewUUID(), + Amount: *fftypes.NewBigInt(1), } pool := &fftypes.TokenPool{ Namespace: "ns1", } - messages := []*fftypes.Message{{ + message := &fftypes.Message{ BatchID: fftypes.NewUUID(), - }} + } - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(pool, nil).Times(2) + mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil).Times(2) + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil).Times(2) mdi.On("UpsertTokenTransfer", em.ctx, transfer).Return(nil).Times(2) mdi.On("UpdateTokenBalances", em.ctx, transfer).Return(nil).Times(2) - mdi.On("GetMessages", em.ctx, mock.Anything).Return(nil, nil, fmt.Errorf("pop")).Once() - mdi.On("GetMessages", em.ctx, mock.Anything).Return(messages, nil, nil).Once() + mdi.On("GetMessageByID", em.ctx, transfer.Message).Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetMessageByID", em.ctx, transfer.Message).Return(message, nil).Once() mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(ev *fftypes.Event) bool { return ev.Type == fftypes.EventTypeTransferConfirmed && ev.Reference == transfer.LocalID && ev.Namespace == pool.Namespace })).Return(nil).Once() @@ -193,27 +231,29 @@ func TestTokensTransferredWithMessageSend(t *testing.T) { mti := &tokenmocks.Plugin{} transfer := &fftypes.TokenTransfer{ - Type: fftypes.TokenTransferTypeTransfer, - TokenIndex: "0", - Connector: "erc1155", - Key: "0x12345", - From: "0x1", - To: "0x2", - MessageHash: fftypes.NewRandB32(), - Amount: *fftypes.NewBigInt(1), + Type: fftypes.TokenTransferTypeTransfer, + TokenIndex: "0", + Connector: "erc1155", + Key: "0x12345", + From: "0x1", + To: "0x2", + ProtocolID: "123", + Message: fftypes.NewUUID(), + Amount: *fftypes.NewBigInt(1), } pool := &fftypes.TokenPool{ Namespace: "ns1", } - messages := []*fftypes.Message{{ + message := &fftypes.Message{ BatchID: fftypes.NewUUID(), State: fftypes.MessageStateStaged, - }} + } - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(pool, nil).Times(2) + mdi.On("GetTokenTransferByProtocolID", em.ctx, "erc1155", "123").Return(nil, nil).Times(2) + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "F1").Return(pool, nil).Times(2) mdi.On("UpsertTokenTransfer", em.ctx, transfer).Return(nil).Times(2) mdi.On("UpdateTokenBalances", em.ctx, transfer).Return(nil).Times(2) - mdi.On("GetMessages", em.ctx, mock.Anything).Return(messages, nil, nil).Times(2) + mdi.On("GetMessageByID", em.ctx, mock.Anything).Return(message, nil).Times(2) mdi.On("UpsertMessage", em.ctx, mock.Anything, true, false).Return(fmt.Errorf("pop")) mdi.On("UpsertMessage", em.ctx, mock.MatchedBy(func(msg *fftypes.Message) bool { return msg.State == fftypes.MessageStateReady diff --git a/internal/i18n/en_translations.go b/internal/i18n/en_translations.go index 2c2cc99db0..948cd13de5 100644 --- a/internal/i18n/en_translations.go +++ b/internal/i18n/en_translations.go @@ -210,4 +210,5 @@ var ( MsgWSClosed = ffm("FF10290", "Websocket closed") MsgTokenTransferFailed = ffm("FF10291", "Token transfer with ID '%s' failed. Please check the FireFly logs for more information") MsgFieldNotSpecified = ffm("FF10292", "Field '%s' must be specified", 400) + MsgTokenPoolNotConfirmed = ffm("FF10293", "Token pool is not yet confirmed") ) diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go index 104fe226a5..4a3684c949 100644 --- a/internal/orchestrator/bound_callbacks.go +++ b/internal/orchestrator/bound_callbacks.go @@ -17,7 +17,6 @@ package orchestrator import ( - "github.com/hyperledger/firefly/internal/assets" "github.com/hyperledger/firefly/internal/events" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/dataexchange" @@ -29,7 +28,6 @@ type boundCallbacks struct { bi blockchain.Plugin dx dataexchange.Plugin ei events.EventManager - am assets.Manager } func (bc *boundCallbacks) BlockchainOpUpdate(operationID *fftypes.UUID, txState blockchain.TransactionStatus, errorMessage string, opOutput fftypes.JSONObject) error { @@ -56,8 +54,8 @@ func (bc *boundCallbacks) MessageReceived(peerID string, data []byte) error { return bc.ei.MessageReceived(bc.dx, peerID, data) } -func (bc *boundCallbacks) TokenPoolCreated(plugin tokens.Plugin, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { - return bc.am.TokenPoolCreated(plugin, pool, protocolTxID, additionalInfo) +func (bc *boundCallbacks) TokenPoolCreated(plugin tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { + return bc.ei.TokenPoolCreated(plugin, pool, protocolTxID, additionalInfo) } func (bc *boundCallbacks) TokensTransferred(plugin tokens.Plugin, poolProtocolID string, transfer *fftypes.TokenTransfer, protocolTxID string, additionalInfo fftypes.JSONObject) error { diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go index c34eb9a805..d8c9a0abf5 100644 --- a/internal/orchestrator/bound_callbacks_test.go +++ b/internal/orchestrator/bound_callbacks_test.go @@ -20,13 +20,13 @@ import ( "fmt" "testing" - "github.com/hyperledger/firefly/mocks/assetmocks" "github.com/hyperledger/firefly/mocks/blockchainmocks" "github.com/hyperledger/firefly/mocks/dataexchangemocks" "github.com/hyperledger/firefly/mocks/eventmocks" "github.com/hyperledger/firefly/mocks/tokenmocks" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/hyperledger/firefly/pkg/tokens" "github.com/stretchr/testify/assert" ) @@ -35,12 +35,11 @@ func TestBoundCallbacks(t *testing.T) { mbi := &blockchainmocks.Plugin{} mdx := &dataexchangemocks.Plugin{} mti := &tokenmocks.Plugin{} - mam := &assetmocks.Manager{} - bc := boundCallbacks{bi: mbi, dx: mdx, ei: mei, am: mam} + bc := boundCallbacks{bi: mbi, dx: mdx, ei: mei} info := fftypes.JSONObject{"hello": "world"} batch := &blockchain.BatchPin{TransactionID: fftypes.NewUUID()} - pool := &fftypes.TokenPool{} + pool := &tokens.TokenPool{} transfer := &fftypes.TokenTransfer{} hash := fftypes.NewRandB32() opID := fftypes.NewUUID() @@ -69,7 +68,7 @@ func TestBoundCallbacks(t *testing.T) { err = bc.MessageReceived("peer1", []byte{}) assert.EqualError(t, err, "pop") - mam.On("TokenPoolCreated", mti, pool, "tx12345", info).Return(fmt.Errorf("pop")) + mei.On("TokenPoolCreated", mti, pool, "tx12345", info).Return(fmt.Errorf("pop")) err = bc.TokenPoolCreated(mti, pool, "tx12345", info) assert.EqualError(t, err, "pop") diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index d45edbad2e..37c5d926b3 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -175,7 +175,6 @@ func (or *orchestrator) Init(ctx context.Context, cancelCtx context.CancelFunc) or.bc.bi = or.blockchain or.bc.ei = or.events or.bc.dx = or.dataexchange - or.bc.am = or.assets return err } @@ -416,7 +415,7 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) { or.syshandlers = syshandlers.NewSystemHandlers(or.database, or.dataexchange, or.data, or.broadcast, or.messaging, or.assets) if or.events == nil { - or.events, err = events.NewEventManager(ctx, or.publicstorage, or.database, or.identity, or.syshandlers, or.data, or.broadcast, or.messaging) + or.events, err = events.NewEventManager(ctx, or.publicstorage, or.database, or.identity, or.syshandlers, or.data, or.broadcast, or.messaging, or.assets) if err != nil { return err } diff --git a/internal/syshandlers/syshandler.go b/internal/syshandlers/syshandler.go index f69334cbab..0061e56487 100644 --- a/internal/syshandlers/syshandler.go +++ b/internal/syshandlers/syshandler.go @@ -35,10 +35,19 @@ import ( type SystemHandlers interface { privatemessaging.GroupManager - HandleSystemBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (valid bool, err error) + HandleSystemBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (SystemBroadcastAction, error) SendReply(ctx context.Context, event *fftypes.Event, reply *fftypes.MessageInOut) } +type SystemBroadcastAction int + +const ( + ActionReject SystemBroadcastAction = iota + ActionConfirm + ActionRetry + ActionWait +) + type systemHandlers struct { database database.Plugin exchange dataexchange.Plugin @@ -77,24 +86,34 @@ func (sh *systemHandlers) EnsureLocalGroup(ctx context.Context, group *fftypes.G return sh.messaging.EnsureLocalGroup(ctx, group) } -func (sh *systemHandlers) HandleSystemBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (valid bool, err error) { +func (sh *systemHandlers) HandleSystemBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (SystemBroadcastAction, error) { l := log.L(ctx) l.Infof("Confirming system broadcast '%s' [%s]", msg.Header.Tag, msg.Header.ID) + var valid bool + var err error switch fftypes.SystemTag(msg.Header.Tag) { case fftypes.SystemTagDefineDatatype: - return sh.handleDatatypeBroadcast(ctx, msg, data) + valid, err = sh.handleDatatypeBroadcast(ctx, msg, data) case fftypes.SystemTagDefineNamespace: - return sh.handleNamespaceBroadcast(ctx, msg, data) + valid, err = sh.handleNamespaceBroadcast(ctx, msg, data) case fftypes.SystemTagDefineOrganization: - return sh.handleOrganizationBroadcast(ctx, msg, data) + valid, err = sh.handleOrganizationBroadcast(ctx, msg, data) case fftypes.SystemTagDefineNode: - return sh.handleNodeBroadcast(ctx, msg, data) + valid, err = sh.handleNodeBroadcast(ctx, msg, data) case fftypes.SystemTagDefinePool: return sh.handleTokenPoolBroadcast(ctx, msg, data) default: l.Warnf("Unknown topic '%s' for system broadcast ID '%s'", msg.Header.Tag, msg.Header.ID) + return ActionReject, nil + } + switch { + case err != nil: + return ActionRetry, err + case !valid: + return ActionReject, nil + default: + return ActionConfirm, nil } - return false, nil } func (sh *systemHandlers) getSystemBroadcastPayload(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data, res fftypes.Definition) (valid bool) { diff --git a/internal/syshandlers/syshandler_datatype_test.go b/internal/syshandlers/syshandler_datatype_test.go index a33ddcb6f4..00e2e97270 100644 --- a/internal/syshandlers/syshandler_datatype_test.go +++ b/internal/syshandlers/syshandler_datatype_test.go @@ -53,12 +53,12 @@ func TestHandleSystemBroadcastDatatypeOk(t *testing.T) { mbi.On("GetDatatypeByName", mock.Anything, "ns1", "name1", "ver1").Return(nil, nil) mbi.On("UpsertDatatype", mock.Anything, mock.Anything, false).Return(nil) mbi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineDatatype), }, }, []*fftypes.Data{data}) - assert.True(t, valid) + assert.Equal(t, ActionConfirm, action) assert.NoError(t, err) mdm.AssertExpectations(t) @@ -89,12 +89,12 @@ func TestHandleSystemBroadcastDatatypeEventFail(t *testing.T) { mbi.On("GetDatatypeByName", mock.Anything, "ns1", "name1", "ver1").Return(nil, nil) mbi.On("UpsertDatatype", mock.Anything, mock.Anything, false).Return(nil) mbi.On("InsertEvent", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineDatatype), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdm.AssertExpectations(t) @@ -118,12 +118,12 @@ func TestHandleSystemBroadcastDatatypeMissingID(t *testing.T) { Value: fftypes.Byteable(b), } - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineDatatype), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } @@ -147,12 +147,12 @@ func TestHandleSystemBroadcastBadSchema(t *testing.T) { mdm := sh.data.(*datamocks.Manager) mdm.On("CheckDatatype", mock.Anything, "ns1", mock.Anything).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineDatatype), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdm.AssertExpectations(t) @@ -171,12 +171,12 @@ func TestHandleSystemBroadcastMissingData(t *testing.T) { } dt.Hash = dt.Value.Hash() - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineDatatype), }, }, []*fftypes.Data{}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } @@ -202,13 +202,13 @@ func TestHandleSystemBroadcastDatatypeLookupFail(t *testing.T) { mdm.On("CheckDatatype", mock.Anything, "ns1", mock.Anything).Return(nil) mbi := sh.database.(*databasemocks.Plugin) mbi.On("GetDatatypeByName", mock.Anything, "ns1", "name1", "ver1").Return(nil, fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: fftypes.SystemNamespace, Tag: string(fftypes.SystemTagDefineDatatype), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdm.AssertExpectations(t) @@ -238,12 +238,12 @@ func TestHandleSystemBroadcastUpsertFail(t *testing.T) { mbi := sh.database.(*databasemocks.Plugin) mbi.On("GetDatatypeByName", mock.Anything, "ns1", "name1", "ver1").Return(nil, nil) mbi.On("UpsertDatatype", mock.Anything, mock.Anything, false).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineDatatype), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdm.AssertExpectations(t) @@ -272,12 +272,12 @@ func TestHandleSystemBroadcastDatatypeDuplicate(t *testing.T) { mdm.On("CheckDatatype", mock.Anything, "ns1", mock.Anything).Return(nil) mbi := sh.database.(*databasemocks.Plugin) mbi.On("GetDatatypeByName", mock.Anything, "ns1", "name1", "ver1").Return(dt, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineDatatype), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdm.AssertExpectations(t) diff --git a/internal/syshandlers/syshandler_namespace_test.go b/internal/syshandlers/syshandler_namespace_test.go index dc47a8e0cc..0f0f757548 100644 --- a/internal/syshandlers/syshandler_namespace_test.go +++ b/internal/syshandlers/syshandler_namespace_test.go @@ -45,12 +45,12 @@ func TestHandleSystemBroadcastNSOk(t *testing.T) { mdi.On("GetNamespace", mock.Anything, "ns1").Return(nil, nil) mdi.On("UpsertNamespace", mock.Anything, mock.Anything, false).Return(nil) mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.True(t, valid) + assert.Equal(t, ActionConfirm, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -73,12 +73,12 @@ func TestHandleSystemBroadcastNSEventFail(t *testing.T) { mdi.On("GetNamespace", mock.Anything, "ns1").Return(nil, nil) mdi.On("UpsertNamespace", mock.Anything, mock.Anything, false).Return(nil) mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -100,12 +100,12 @@ func TestHandleSystemBroadcastNSUpsertFail(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetNamespace", mock.Anything, "ns1").Return(nil, nil) mdi.On("UpsertNamespace", mock.Anything, mock.Anything, false).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -114,12 +114,12 @@ func TestHandleSystemBroadcastNSUpsertFail(t *testing.T) { func TestHandleSystemBroadcastNSMissingData(t *testing.T) { sh := newTestSystemHandlers(t) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } @@ -133,12 +133,12 @@ func TestHandleSystemBroadcastNSBadID(t *testing.T) { Value: fftypes.Byteable(b), } - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } @@ -149,12 +149,12 @@ func TestHandleSystemBroadcastNSBadData(t *testing.T) { Value: fftypes.Byteable(`!{json`), } - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } @@ -173,12 +173,12 @@ func TestHandleSystemBroadcastDuplicate(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetNamespace", mock.Anything, "ns1").Return(ns, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -203,12 +203,12 @@ func TestHandleSystemBroadcastDuplicateOverrideLocal(t *testing.T) { mdi.On("DeleteNamespace", mock.Anything, mock.Anything).Return(nil) mdi.On("UpsertNamespace", mock.Anything, mock.Anything, false).Return(nil) mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.True(t, valid) + assert.Equal(t, ActionConfirm, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -231,12 +231,12 @@ func TestHandleSystemBroadcastDuplicateOverrideLocalFail(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetNamespace", mock.Anything, "ns1").Return(ns, nil) mdi.On("DeleteNamespace", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -257,12 +257,12 @@ func TestHandleSystemBroadcastDupCheckFail(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetNamespace", mock.Anything, "ns1").Return(nil, fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Tag: string(fftypes.SystemTagDefineNamespace), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) diff --git a/internal/syshandlers/syshandler_network_node_test.go b/internal/syshandlers/syshandler_network_node_test.go index 3c420070c1..868a88a1a2 100644 --- a/internal/syshandlers/syshandler_network_node_test.go +++ b/internal/syshandlers/syshandler_network_node_test.go @@ -55,7 +55,7 @@ func TestHandleSystemBroadcastNodeOk(t *testing.T) { mdi.On("UpsertNode", mock.Anything, mock.Anything, true).Return(nil) mdx := sh.exchange.(*dataexchangemocks.Plugin) mdx.On("AddPeer", mock.Anything, "peer1", node.DX.Endpoint).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -65,7 +65,7 @@ func TestHandleSystemBroadcastNodeOk(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.True(t, valid) + assert.Equal(t, ActionConfirm, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -95,7 +95,7 @@ func TestHandleSystemBroadcastNodeUpsertFail(t *testing.T) { mdi.On("GetNode", mock.Anything, "0x23456", "node1").Return(nil, nil) mdi.On("GetNodeByID", mock.Anything, node.ID).Return(nil, nil) mdi.On("UpsertNode", mock.Anything, mock.Anything, true).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -105,7 +105,7 @@ func TestHandleSystemBroadcastNodeUpsertFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -137,7 +137,7 @@ func TestHandleSystemBroadcastNodeAddPeerFail(t *testing.T) { mdi.On("UpsertNode", mock.Anything, mock.Anything, true).Return(nil) mdx := sh.exchange.(*dataexchangemocks.Plugin) mdx.On("AddPeer", mock.Anything, "peer1", mock.Anything).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -147,7 +147,7 @@ func TestHandleSystemBroadcastNodeAddPeerFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -175,7 +175,7 @@ func TestHandleSystemBroadcastNodeDupMismatch(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(&fftypes.Organization{ID: fftypes.NewUUID(), Identity: "0x23456"}, nil) mdi.On("GetNode", mock.Anything, "0x23456", "node1").Return(&fftypes.Node{Owner: "0x99999"}, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -185,7 +185,7 @@ func TestHandleSystemBroadcastNodeDupMismatch(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -216,7 +216,7 @@ func TestHandleSystemBroadcastNodeDupOK(t *testing.T) { mdi.On("UpsertNode", mock.Anything, mock.Anything, true).Return(nil) mdx := sh.exchange.(*dataexchangemocks.Plugin) mdx.On("AddPeer", mock.Anything, "peer1", mock.Anything).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -226,7 +226,7 @@ func TestHandleSystemBroadcastNodeDupOK(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.True(t, valid) + assert.Equal(t, ActionConfirm, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -254,7 +254,7 @@ func TestHandleSystemBroadcastNodeGetFail(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(&fftypes.Organization{ID: fftypes.NewUUID(), Identity: "0x23456"}, nil) mdi.On("GetNode", mock.Anything, "0x23456", "node1").Return(nil, fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -264,7 +264,7 @@ func TestHandleSystemBroadcastNodeGetFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -291,7 +291,7 @@ func TestHandleSystemBroadcastNodeBadAuthor(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(&fftypes.Organization{ID: fftypes.NewUUID(), Identity: "0x23456"}, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -301,7 +301,7 @@ func TestHandleSystemBroadcastNodeBadAuthor(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -328,7 +328,7 @@ func TestHandleSystemBroadcastNodeGetOrgNotFound(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(nil, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -338,7 +338,7 @@ func TestHandleSystemBroadcastNodeGetOrgNotFound(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -365,7 +365,7 @@ func TestHandleSystemBroadcastNodeGetOrgFail(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(nil, fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -375,7 +375,7 @@ func TestHandleSystemBroadcastNodeGetOrgFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -400,7 +400,7 @@ func TestHandleSystemBroadcastNodeValidateFail(t *testing.T) { Value: fftypes.Byteable(b), } - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -410,7 +410,7 @@ func TestHandleSystemBroadcastNodeValidateFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } @@ -421,7 +421,7 @@ func TestHandleSystemBroadcastNodeUnmarshalFail(t *testing.T) { Value: fftypes.Byteable(`!json`), } - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -431,6 +431,6 @@ func TestHandleSystemBroadcastNodeUnmarshalFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineNode), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } diff --git a/internal/syshandlers/syshandler_network_org_test.go b/internal/syshandlers/syshandler_network_org_test.go index 0c7818c80c..2636ff5a2e 100644 --- a/internal/syshandlers/syshandler_network_org_test.go +++ b/internal/syshandlers/syshandler_network_org_test.go @@ -58,7 +58,7 @@ func TestHandleSystemBroadcastChildOrgOk(t *testing.T) { mdi.On("GetOrganizationByName", mock.Anything, "org1").Return(nil, nil) mdi.On("GetOrganizationByID", mock.Anything, org.ID).Return(nil, nil) mdi.On("UpsertOrganization", mock.Anything, mock.Anything, true).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -68,7 +68,7 @@ func TestHandleSystemBroadcastChildOrgOk(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.True(t, valid) + assert.Equal(t, ActionConfirm, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -102,7 +102,7 @@ func TestHandleSystemBroadcastChildOrgDupOk(t *testing.T) { mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(parentOrg, nil) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x12345").Return(org, nil) mdi.On("UpsertOrganization", mock.Anything, mock.Anything, true).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -112,7 +112,7 @@ func TestHandleSystemBroadcastChildOrgDupOk(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.True(t, valid) + assert.Equal(t, ActionConfirm, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -144,7 +144,7 @@ func TestHandleSystemBroadcastChildOrgBadKey(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(parentOrg, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -154,7 +154,7 @@ func TestHandleSystemBroadcastChildOrgBadKey(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -179,7 +179,7 @@ func TestHandleSystemBroadcastOrgDupMismatch(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x12345").Return(&fftypes.Organization{ID: fftypes.NewUUID(), Identity: "0x12345", Parent: "0x9999"}, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -189,7 +189,7 @@ func TestHandleSystemBroadcastOrgDupMismatch(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -216,7 +216,7 @@ func TestHandleSystemBroadcastOrgUpsertFail(t *testing.T) { mdi.On("GetOrganizationByName", mock.Anything, "org1").Return(nil, nil) mdi.On("GetOrganizationByID", mock.Anything, org.ID).Return(nil, nil) mdi.On("UpsertOrganization", mock.Anything, mock.Anything, true).Return(fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -225,7 +225,7 @@ func TestHandleSystemBroadcastOrgUpsertFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -249,7 +249,7 @@ func TestHandleSystemBroadcastOrgGetOrgFail(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x12345").Return(nil, fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -258,7 +258,7 @@ func TestHandleSystemBroadcastOrgGetOrgFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -282,7 +282,7 @@ func TestHandleSystemBroadcastOrgAuthorMismatch(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x12345").Return(&fftypes.Organization{ID: fftypes.NewUUID(), Identity: "0x12345", Parent: "0x9999"}, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -292,7 +292,7 @@ func TestHandleSystemBroadcastOrgAuthorMismatch(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -317,7 +317,7 @@ func TestHandleSystemBroadcastGetParentFail(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(nil, fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -327,7 +327,7 @@ func TestHandleSystemBroadcastGetParentFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -352,7 +352,7 @@ func TestHandleSystemBroadcastGetParentNotFound(t *testing.T) { mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOrganizationByIdentity", mock.Anything, "0x23456").Return(nil, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -362,7 +362,7 @@ func TestHandleSystemBroadcastGetParentNotFound(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -383,7 +383,7 @@ func TestHandleSystemBroadcastValidateFail(t *testing.T) { Value: fftypes.Byteable(b), } - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -393,7 +393,7 @@ func TestHandleSystemBroadcastValidateFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } @@ -404,7 +404,7 @@ func TestHandleSystemBroadcastUnmarshalFail(t *testing.T) { Value: fftypes.Byteable(`!json`), } - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ Namespace: "ns1", Identity: fftypes.Identity{ @@ -414,6 +414,6 @@ func TestHandleSystemBroadcastUnmarshalFail(t *testing.T) { Tag: string(fftypes.SystemTagDefineOrganization), }, }, []*fftypes.Data{data}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } diff --git a/internal/syshandlers/syshandler_test.go b/internal/syshandlers/syshandler_test.go index 5bf7157fef..47f82e2d9a 100644 --- a/internal/syshandlers/syshandler_test.go +++ b/internal/syshandlers/syshandler_test.go @@ -43,12 +43,12 @@ func newTestSystemHandlers(t *testing.T) *systemHandlers { func TestHandleSystemBroadcastUnknown(t *testing.T) { sh := newTestSystemHandlers(t) - valid, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ + action, err := sh.HandleSystemBroadcast(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ - Tag: "uknown", + Tag: "unknown", }, }, []*fftypes.Data{}) - assert.False(t, valid) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } @@ -56,7 +56,7 @@ func TestGetSystemBroadcastPayloadMissingData(t *testing.T) { sh := newTestSystemHandlers(t) valid := sh.getSystemBroadcastPayload(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ - Tag: "uknown", + Tag: "unknown", }, }, []*fftypes.Data{}, nil) assert.False(t, valid) @@ -66,7 +66,7 @@ func TestGetSystemBroadcastPayloadBadJSON(t *testing.T) { sh := newTestSystemHandlers(t) valid := sh.getSystemBroadcastPayload(context.Background(), &fftypes.Message{ Header: fftypes.MessageHeader{ - Tag: "uknown", + Tag: "unknown", }, }, []*fftypes.Data{}, nil) assert.False(t, valid) diff --git a/internal/syshandlers/syshandler_tokenpool.go b/internal/syshandlers/syshandler_tokenpool.go index 8942e8562c..054c694ac5 100644 --- a/internal/syshandlers/syshandler_tokenpool.go +++ b/internal/syshandlers/syshandler_tokenpool.go @@ -24,69 +24,38 @@ import ( "github.com/hyperledger/firefly/pkg/fftypes" ) -func (sh *systemHandlers) persistTokenPool(ctx context.Context, pool *fftypes.TokenPoolAnnouncement) (valid bool, err error) { +func (sh *systemHandlers) confirmPoolAnnounceOp(ctx context.Context, pool *fftypes.TokenPool) error { // Find a matching operation within this transaction fb := database.OperationQueryFactory.NewFilter(ctx) filter := fb.And( fb.Eq("tx", pool.TX.ID), fb.Eq("type", fftypes.OpTypeTokenAnnouncePool), ) - operations, _, err := sh.database.GetOperations(ctx, filter) - if err != nil { - return false, err // retryable - } - - if len(operations) > 0 { - // Mark announce operation completed + if operations, _, err := sh.database.GetOperations(ctx, filter); err != nil { + return err + } else if len(operations) > 0 { + op := operations[0] update := database.OperationQueryFactory.NewUpdate(ctx). Set("status", fftypes.OpStatusSucceeded). Set("output", fftypes.JSONObject{"message": pool.Message}) - if err := sh.database.UpdateOperation(ctx, operations[0].ID, update); err != nil { - return false, err // retryable + if err := sh.database.UpdateOperation(ctx, op.ID, update); err != nil { + return err } + } + return nil +} - // Validate received info matches the database - transaction, err := sh.database.GetTransactionByID(ctx, pool.TX.ID) - if err != nil { - return false, err // retryable - } - if transaction.ProtocolID != pool.ProtocolTxID { - log.L(ctx).Warnf("Ignoring token pool from transaction '%s' - unexpected protocol ID '%s'", pool.TX.ID, pool.ProtocolTxID) - return false, nil // not retryable - } +func (sh *systemHandlers) persistTokenPool(ctx context.Context, announce *fftypes.TokenPoolAnnouncement) (valid bool, err error) { + pool := announce.Pool - // Mark transaction completed - transaction.Status = fftypes.OpStatusSucceeded - err = sh.database.UpsertTransaction(ctx, transaction, false) - if err != nil { - return false, err // retryable - } - } else { - // No local announce operation found (broadcast originated from another node) - log.L(ctx).Infof("Validating token pool transaction '%s' with protocol ID '%s'", pool.TX.ID, pool.ProtocolTxID) - err = sh.assets.ValidateTokenPoolTx(ctx, &pool.TokenPool, pool.ProtocolTxID) - if err != nil { - log.L(ctx).Errorf("Failed to validate token pool transaction '%s': %v", pool.TX.ID, err) - return false, err // retryable - } - transaction := &fftypes.Transaction{ - ID: pool.TX.ID, - Status: fftypes.OpStatusSucceeded, - Subject: fftypes.TransactionSubject{ - Namespace: pool.Namespace, - Type: fftypes.TransactionTypeTokenPool, - Signer: pool.Key, - Reference: pool.ID, - }, - ProtocolID: pool.ProtocolTxID, - } - valid, err = sh.txhelper.PersistTransaction(ctx, transaction) - if !valid || err != nil { - return valid, err - } + // Mark announce operation (if any) completed + if err := sh.confirmPoolAnnounceOp(ctx, pool); err != nil { + return false, err // retryable } - err = sh.database.UpsertTokenPool(ctx, &pool.TokenPool) + // Create the pool in pending state + pool.State = fftypes.TokenPoolStatePending + err = sh.database.UpsertTokenPool(ctx, pool) if err != nil { if err == database.IDMismatch { log.L(ctx).Errorf("Invalid token pool '%s'. ID mismatch with existing record", pool.ID) @@ -95,35 +64,48 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, pool *fftypes.To log.L(ctx).Errorf("Failed to insert token pool '%s': %s", pool.ID, err) return false, err // retryable } + return true, nil } -func (sh *systemHandlers) handleTokenPoolBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (valid bool, err error) { - l := log.L(ctx) - - var pool fftypes.TokenPoolAnnouncement - valid = sh.getSystemBroadcastPayload(ctx, msg, data, &pool) - if valid { - if err = pool.Validate(ctx, true); err != nil { - l.Warnf("Unable to process token pool broadcast %s - validate failed: %s", msg.Header.ID, err) - valid = false - } else { - pool.Message = msg.Header.ID - valid, err = sh.persistTokenPool(ctx, &pool) - if err != nil { - return valid, err - } - } +func (sh *systemHandlers) rejectPool(ctx context.Context, pool *fftypes.TokenPool) error { + event := fftypes.NewEvent(fftypes.EventTypePoolRejected, pool.Namespace, pool.ID) + err := sh.database.InsertEvent(ctx, event) + return err +} + +func (sh *systemHandlers) handleTokenPoolBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (SystemBroadcastAction, error) { + var announce fftypes.TokenPoolAnnouncement + if valid := sh.getSystemBroadcastPayload(ctx, msg, data, &announce); !valid { + return ActionReject, nil } - var event *fftypes.Event - if valid { - l.Infof("Token pool created id=%s author=%s", pool.ID, msg.Header.Author) - event = fftypes.NewEvent(fftypes.EventTypePoolConfirmed, pool.Namespace, pool.ID) - } else { - l.Warnf("Token pool rejected id=%s author=%s", pool.ID, msg.Header.Author) - event = fftypes.NewEvent(fftypes.EventTypePoolRejected, pool.Namespace, pool.ID) + pool := announce.Pool + pool.Message = msg.Header.ID + + if err := pool.Validate(ctx); err != nil { + log.L(ctx).Warnf("Token pool '%s' rejected - validate failed: %s", pool.ID, err) + return ActionReject, sh.rejectPool(ctx, pool) } - err = sh.database.InsertEvent(ctx, event) - return valid, err + + // Check if pool has already been confirmed on chain (and confirm the message if so) + if existingPool, err := sh.database.GetTokenPoolByID(ctx, pool.ID); err != nil { + return ActionRetry, err + } else if existingPool != nil && existingPool.State == fftypes.TokenPoolStateConfirmed { + return ActionConfirm, nil + } + + if valid, err := sh.persistTokenPool(ctx, &announce); err != nil { + return ActionRetry, err + } else if !valid { + return ActionReject, sh.rejectPool(ctx, pool) + } + + if err := sh.assets.ActivateTokenPool(ctx, pool, announce.TX); err != nil { + log.L(ctx).Errorf("Failed to activate token pool '%s': %s", pool.ID, err) + return ActionRetry, err + } + + // Message will remain unconfirmed until pool confirmation triggers a rewind + return ActionWait, nil } diff --git a/internal/syshandlers/syshandler_tokenpool_test.go b/internal/syshandlers/syshandler_tokenpool_test.go index b316996d4d..01852c0b07 100644 --- a/internal/syshandlers/syshandler_tokenpool_test.go +++ b/internal/syshandlers/syshandler_tokenpool_test.go @@ -30,572 +30,299 @@ import ( "github.com/stretchr/testify/mock" ) -func TestHandleSystemBroadcastTokenPoolSelfOk(t *testing.T) { - sh := newTestSystemHandlers(t) - - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, +func newPoolAnnouncement() *fftypes.TokenPoolAnnouncement { + pool := &fftypes.TokenPool{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + Name: "name1", + Type: fftypes.TokenTypeFungible, + ProtocolID: "12345", + Symbol: "COIN", + TX: fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: fftypes.NewUUID(), }, - ProtocolTxID: "tx123", } + return &fftypes.TokenPoolAnnouncement{ + Pool: pool, + TX: &fftypes.Transaction{}, + } +} + +func buildPoolDefinitionMessage(announce *fftypes.TokenPoolAnnouncement) (*fftypes.Message, []*fftypes.Data, error) { msg := &fftypes.Message{ Header: fftypes.MessageHeader{ ID: fftypes.NewUUID(), Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) - assert.NoError(t, err) + b, err := json.Marshal(announce) + if err != nil { + return nil, nil, err + } data := []*fftypes.Data{{ Value: fftypes.Byteable(b), }} + return msg, data, nil +} + +func TestHandleSystemBroadcastTokenPoolActivateOK(t *testing.T) { + sh := newTestSystemHandlers(t) + + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) + assert.NoError(t, err) opID := fftypes.NewUUID() operations := []*fftypes.Operation{{ID: opID}} - tx := &fftypes.Transaction{ProtocolID: "tx123"} mdi := sh.database.(*databasemocks.Plugin) + mam := sh.assets.(*assetmocks.Manager) mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) mdi.On("UpdateOperation", context.Background(), opID, mock.Anything).Return(nil) - mdi.On("GetTransactionByID", context.Background(), pool.TX.ID).Return(tx, nil) - mdi.On("UpsertTransaction", context.Background(), tx, false).Return(nil) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(nil, nil) mdi.On("UpsertTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { return *p.ID == *pool.ID && p.Message == msg.Header.ID })).Return(nil) - mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *fftypes.Event) bool { - return *event.Reference == *pool.ID && event.Namespace == pool.Namespace && event.Type == fftypes.EventTypePoolConfirmed + mam.On("ActivateTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { + return true + }), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return true })).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.True(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionWait, action) assert.NoError(t, err) - assert.Equal(t, fftypes.OpStatusSucceeded, tx.Status) mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolSelfUpdateOpFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolUpdateOpFail(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} opID := fftypes.NewUUID() operations := []*fftypes.Operation{{ID: opID}} mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) mdi.On("UpdateOperation", context.Background(), opID, mock.Anything).Return(fmt.Errorf("pop")) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(nil, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolSelfGetTXFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolGetPoolFail(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - opID := fftypes.NewUUID() - operations := []*fftypes.Operation{{ID: opID}} mdi := sh.database.(*databasemocks.Plugin) - mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) - mdi.On("UpdateOperation", context.Background(), opID, mock.Anything).Return(nil) - mdi.On("GetTransactionByID", context.Background(), pool.TX.ID).Return(nil, fmt.Errorf("pop")) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(nil, fmt.Errorf("pop")) - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolSelfTXMismatch(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolExisting(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - opID := fftypes.NewUUID() - operations := []*fftypes.Operation{{ID: opID}} - tx := &fftypes.Transaction{ProtocolID: "bad"} + operations := []*fftypes.Operation{} mdi := sh.database.(*databasemocks.Plugin) + mam := sh.assets.(*assetmocks.Manager) mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) - mdi.On("UpdateOperation", context.Background(), opID, mock.Anything).Return(nil) - mdi.On("GetTransactionByID", context.Background(), pool.TX.ID).Return(tx, nil) - mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *fftypes.Event) bool { - return *event.Reference == *pool.ID && event.Namespace == pool.Namespace && event.Type == fftypes.EventTypePoolRejected + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(&fftypes.TokenPool{}, nil) + mdi.On("UpsertTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { + return *p.ID == *pool.ID && p.Message == msg.Header.ID + })).Return(nil) + mam.On("ActivateTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { + return true + }), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return true })).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionWait, action) assert.NoError(t, err) mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolSelfUpdateTXFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolExistingConfirmed(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - opID := fftypes.NewUUID() - operations := []*fftypes.Operation{{ID: opID}} - tx := &fftypes.Transaction{ProtocolID: "tx123"} + existing := &fftypes.TokenPool{ + State: fftypes.TokenPoolStateConfirmed, + } mdi := sh.database.(*databasemocks.Plugin) - mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) - mdi.On("UpdateOperation", context.Background(), opID, mock.Anything).Return(nil) - mdi.On("GetTransactionByID", context.Background(), pool.TX.ID).Return(tx, nil) - mdi.On("UpsertTransaction", context.Background(), tx, false).Return(fmt.Errorf("pop")) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(existing, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) - assert.EqualError(t, err, "pop") + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionConfirm, action) + assert.NoError(t, err) mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolOk(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolIDMismatch(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - operations := []*fftypes.Operation{} + opID := fftypes.NewUUID() + operations := []*fftypes.Operation{{ID: opID}} mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) - mdi.On("GetTransactionByID", context.Background(), pool.TX.ID).Return(nil, nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(t *fftypes.Transaction) bool { - return t.Subject.Type == fftypes.TransactionTypeTokenPool && *t.Subject.Reference == *pool.ID - }), false).Return(nil) + mdi.On("UpdateOperation", context.Background(), opID, mock.Anything).Return(nil) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(nil, nil) mdi.On("UpsertTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { return *p.ID == *pool.ID && p.Message == msg.Header.ID - })).Return(nil) + })).Return(database.IDMismatch) mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *fftypes.Event) bool { - return *event.Reference == *pool.ID && event.Namespace == pool.Namespace && event.Type == fftypes.EventTypePoolConfirmed + return *event.Reference == *pool.ID && event.Namespace == pool.Namespace && event.Type == fftypes.EventTypePoolRejected })).Return(nil) - mam := sh.assets.(*assetmocks.Manager) - mam.On("ValidateTokenPoolTx", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { - return *p.ID == *pool.ID - }), "tx123").Return(nil) - - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.True(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) - mam.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolValidateTxFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolFailUpsert(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - operations := []*fftypes.Operation{} + opID := fftypes.NewUUID() + operations := []*fftypes.Operation{{ID: opID}} mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) + mdi.On("UpdateOperation", context.Background(), opID, mock.Anything).Return(nil) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(nil, nil) + mdi.On("UpsertTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { + return *p.ID == *pool.ID && p.Message == msg.Header.ID + })).Return(fmt.Errorf("pop")) - mam := sh.assets.(*assetmocks.Manager) - mam.On("ValidateTokenPoolTx", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { - return *p.ID == *pool.ID - }), "tx123").Return(fmt.Errorf("pop")) - - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) - mam.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolBadTX(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolOpsFail(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: nil, - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - operations := []*fftypes.Operation{} mdi := sh.database.(*databasemocks.Plugin) - mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) - mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *fftypes.Event) bool { - return *event.Reference == *pool.ID && event.Namespace == pool.Namespace && event.Type == fftypes.EventTypePoolRejected - })).Return(nil) - - mam := sh.assets.(*assetmocks.Manager) - mam.On("ValidateTokenPoolTx", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { - return *p.ID == *pool.ID - }), "tx123").Return(nil) + mdi.On("GetOperations", context.Background(), mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(nil, nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) - assert.NoError(t, err) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionRetry, action) + assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) - mam.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolIDMismatch(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolActivateFail(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - operations := []*fftypes.Operation{} + opID := fftypes.NewUUID() + operations := []*fftypes.Operation{{ID: opID}} mdi := sh.database.(*databasemocks.Plugin) - mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) - mdi.On("GetTransactionByID", context.Background(), pool.TX.ID).Return(nil, nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(t *fftypes.Transaction) bool { - return t.Subject.Type == fftypes.TransactionTypeTokenPool && *t.Subject.Reference == *pool.ID - }), false).Return(nil) - mdi.On("UpsertTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { - return *p.ID == *pool.ID && p.Message == msg.Header.ID - })).Return(database.IDMismatch) - mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *fftypes.Event) bool { - return *event.Reference == *pool.ID && event.Namespace == pool.Namespace && event.Type == fftypes.EventTypePoolRejected - })).Return(nil) - mam := sh.assets.(*assetmocks.Manager) - mam.On("ValidateTokenPoolTx", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { - return *p.ID == *pool.ID - }), "tx123").Return(nil) - - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mam.AssertExpectations(t) -} - -func TestHandleSystemBroadcastTokenPoolFailUpsert(t *testing.T) { - sh := newTestSystemHandlers(t) - - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) - assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - operations := []*fftypes.Operation{} - - mdi := sh.database.(*databasemocks.Plugin) mdi.On("GetOperations", context.Background(), mock.Anything).Return(operations, nil, nil) - mdi.On("GetTransactionByID", context.Background(), pool.TX.ID).Return(nil, nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(t *fftypes.Transaction) bool { - return t.Subject.Type == fftypes.TransactionTypeTokenPool && *t.Subject.Reference == *pool.ID - }), false).Return(nil) + mdi.On("UpdateOperation", context.Background(), opID, mock.Anything).Return(nil) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(nil, nil) mdi.On("UpsertTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { return *p.ID == *pool.ID && p.Message == msg.Header.ID + })).Return(nil) + mam.On("ActivateTokenPool", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { + return true + }), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return true })).Return(fmt.Errorf("pop")) - mam := sh.assets.(*assetmocks.Manager) - mam.On("ValidateTokenPoolTx", context.Background(), mock.MatchedBy(func(p *fftypes.TokenPool) bool { - return *p.ID == *pool.ID - }), "tx123").Return(nil) - - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) - mam.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolOpsFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolValidateFail(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - }, - ProtocolTxID: "tx123", + announce := &fftypes.TokenPoolAnnouncement{ + Pool: &fftypes.TokenPool{}, + TX: &fftypes.Transaction{}, } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&pool) + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} mdi := sh.database.(*databasemocks.Plugin) - mdi.On("GetOperations", context.Background(), mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *fftypes.Event) bool { + return event.Type == fftypes.EventTypePoolRejected + })).Return(nil) - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) - assert.EqualError(t, err, "pop") + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionReject, action) + assert.NoError(t, err) mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolValidateFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolBadMessage(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{}, - ProtocolTxID: "tx123", - } msg := &fftypes.Message{ Header: fftypes.MessageHeader{ ID: fftypes.NewUUID(), Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) - assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} - mdi := sh.database.(*databasemocks.Plugin) - mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *fftypes.Event) bool { - return event.Type == fftypes.EventTypePoolRejected - })).Return(nil) - - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, nil) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) - - mdi.AssertExpectations(t) } diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go index 7ce8d3f716..dc863bedc0 100644 --- a/internal/tokens/fftokens/fftokens.go +++ b/internal/tokens/fftokens/fftokens.go @@ -56,22 +56,33 @@ const ( messageTokenTransfer msgType = "token-transfer" ) +type tokenData struct { + TX *fftypes.UUID `json:"tx,omitempty"` + Message *fftypes.UUID `json:"message,omitempty"` + MessageHash *fftypes.Bytes32 `json:"messageHash,omitempty"` +} + type createPool struct { - Type fftypes.TokenType `json:"type"` - RequestID string `json:"requestId"` - TrackingID string `json:"trackingId"` - Operator string `json:"operator"` - Config fftypes.JSONObject `json:"config"` + Type fftypes.TokenType `json:"type"` + RequestID string `json:"requestId"` + Operator string `json:"operator"` + Data string `json:"data,omitempty"` + Config fftypes.JSONObject `json:"config"` +} + +type activatePool struct { + PoolID string `json:"poolId"` + Transaction fftypes.JSONObject `json:"transaction"` + RequestID string `json:"requestId,omitempty"` } type mintTokens struct { - PoolID string `json:"poolId"` - To string `json:"to"` - Amount string `json:"amount"` - RequestID string `json:"requestId,omitempty"` - TrackingID string `json:"trackingId"` - Operator string `json:"operator"` - Data string `json:"data,omitempty"` + PoolID string `json:"poolId"` + To string `json:"to"` + Amount string `json:"amount"` + RequestID string `json:"requestId,omitempty"` + Operator string `json:"operator"` + Data string `json:"data,omitempty"` } type burnTokens struct { @@ -80,7 +91,6 @@ type burnTokens struct { From string `json:"from"` Amount string `json:"amount"` RequestID string `json:"requestId,omitempty"` - TrackingID string `json:"trackingId"` Operator string `json:"operator"` Data string `json:"data,omitempty"` } @@ -92,7 +102,6 @@ type transferTokens struct { To string `json:"to"` Amount string `json:"amount"` RequestID string `json:"requestId,omitempty"` - TrackingID string `json:"trackingId"` Operator string `json:"operator"` Data string `json:"data,omitempty"` } @@ -164,36 +173,34 @@ func (ft *FFTokens) handleTokenPoolCreate(ctx context.Context, data fftypes.JSON tokenType := data.GetString("type") protocolID := data.GetString("poolId") standard := data.GetString("standard") // this is optional - trackingID := data.GetString("trackingId") operatorAddress := data.GetString("operator") tx := data.GetObject("transaction") txHash := tx.GetString("transactionHash") if tokenType == "" || protocolID == "" || - trackingID == "" || operatorAddress == "" || txHash == "" { log.L(ctx).Errorf("TokenPool event is not valid - missing data: %+v", data) return nil // move on } - txID, err := fftypes.ParseUUID(ctx, trackingID) - if err != nil { - log.L(ctx).Errorf("TokenPool event is not valid - invalid transaction ID (%s): %+v", err, data) - return nil // move on + // We want to process all events, even those not initiated by FireFly. + // The "data" argument is optional, so it's important not to fail if it's missing or malformed. + poolDataString := data.GetString("data") + var poolData tokenData + if err = json.Unmarshal([]byte(poolDataString), &poolData); err != nil { + log.L(ctx).Infof("TokenPool event data could not be parsed - continuing anyway (%s): %+v", err, data) + poolData = tokenData{} } - pool := &fftypes.TokenPool{ - Type: fftypes.FFEnum(tokenType), - ProtocolID: protocolID, - Standard: standard, - Connector: ft.configuredName, - Key: operatorAddress, - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, + pool := &tokens.TokenPool{ + Type: fftypes.FFEnum(tokenType), + ProtocolID: protocolID, + TransactionID: poolData.TX, + Key: operatorAddress, + Connector: ft.configuredName, + Standard: standard, } // If there's an error dispatching the event, we must return the error and shutdown @@ -230,22 +237,13 @@ func (ft *FFTokens) handleTokenTransfer(ctx context.Context, t fftypes.TokenTran return nil // move on } - // We want to process all transfers, even those not initiated by FireFly. - // The following are optional arguments from the connector, so it's important not to - // fail if they're missing or malformed. - trackingID := data.GetString("trackingId") - txID, err := fftypes.ParseUUID(ctx, trackingID) - if err != nil { - log.L(ctx).Infof("%s event contains invalid ID - continuing anyway (%s): %+v", eventName, err, data) - txID = fftypes.NewUUID() - } - transferData := data.GetString("data") - var messageHash fftypes.Bytes32 - if transferData != "" { - err = messageHash.UnmarshalText([]byte(transferData)) - if err != nil { - log.L(ctx).Errorf("%s event contains invalid message hash - continuing anyway (%s): %+v", eventName, err, data) - } + // We want to process all events, even those not initiated by FireFly. + // The "data" argument is optional, so it's important not to fail if it's missing or malformed. + transferDataString := data.GetString("data") + var transferData tokenData + if err = json.Unmarshal([]byte(transferDataString), &transferData); err != nil { + log.L(ctx).Infof("%s event data could not be parsed - continuing anyway (%s): %+v", eventName, err, data) + transferData = tokenData{} } transfer := &fftypes.TokenTransfer{ @@ -256,9 +254,10 @@ func (ft *FFTokens) handleTokenTransfer(ctx context.Context, t fftypes.TokenTran To: toAddress, ProtocolID: txHash, Key: operatorAddress, - MessageHash: &messageHash, + Message: transferData.Message, + MessageHash: transferData.MessageHash, TX: fftypes.TransactionRef{ - ID: txID, + ID: transferData.TX, Type: fftypes.TransactionTypeTokenTransfer, }, } @@ -330,15 +329,32 @@ func (ft *FFTokens) eventLoop() { } func (ft *FFTokens) CreateTokenPool(ctx context.Context, operationID *fftypes.UUID, pool *fftypes.TokenPool) error { + data, _ := json.Marshal(tokenData{ + TX: pool.TX.ID, + }) res, err := ft.client.R().SetContext(ctx). SetBody(&createPool{ - Type: pool.Type, - RequestID: operationID.String(), - TrackingID: pool.TX.ID.String(), - Operator: pool.Key, - Config: pool.Config, + Type: pool.Type, + RequestID: operationID.String(), + Operator: pool.Key, + Data: string(data), + Config: pool.Config, + }). + Post("/api/v1/createpool") + if err != nil || !res.IsSuccess() { + return restclient.WrapRestErr(ctx, res, err, i18n.MsgTokensRESTErr) + } + return nil +} + +func (ft *FFTokens) ActivateTokenPool(ctx context.Context, operationID *fftypes.UUID, pool *fftypes.TokenPool, tx *fftypes.Transaction) error { + res, err := ft.client.R().SetContext(ctx). + SetBody(&activatePool{ + RequestID: operationID.String(), + PoolID: pool.ProtocolID, + Transaction: tx.Info, }). - Post("/api/v1/pool") + Post("/api/v1/activatepool") if err != nil || !res.IsSuccess() { return restclient.WrapRestErr(ctx, res, err, i18n.MsgTokensRESTErr) } @@ -346,15 +362,19 @@ func (ft *FFTokens) CreateTokenPool(ctx context.Context, operationID *fftypes.UU } func (ft *FFTokens) MintTokens(ctx context.Context, operationID *fftypes.UUID, poolProtocolID string, mint *fftypes.TokenTransfer) error { + data, _ := json.Marshal(tokenData{ + TX: mint.TX.ID, + Message: mint.Message, + MessageHash: mint.MessageHash, + }) res, err := ft.client.R().SetContext(ctx). SetBody(&mintTokens{ - PoolID: poolProtocolID, - To: mint.To, - Amount: mint.Amount.Int().String(), - RequestID: operationID.String(), - TrackingID: mint.TX.ID.String(), - Operator: mint.Key, - Data: mint.MessageHash.String(), + PoolID: poolProtocolID, + To: mint.To, + Amount: mint.Amount.Int().String(), + RequestID: operationID.String(), + Operator: mint.Key, + Data: string(data), }). Post("/api/v1/mint") if err != nil || !res.IsSuccess() { @@ -364,6 +384,11 @@ func (ft *FFTokens) MintTokens(ctx context.Context, operationID *fftypes.UUID, p } func (ft *FFTokens) BurnTokens(ctx context.Context, operationID *fftypes.UUID, poolProtocolID string, burn *fftypes.TokenTransfer) error { + data, _ := json.Marshal(tokenData{ + TX: burn.TX.ID, + Message: burn.Message, + MessageHash: burn.MessageHash, + }) res, err := ft.client.R().SetContext(ctx). SetBody(&burnTokens{ PoolID: poolProtocolID, @@ -371,9 +396,8 @@ func (ft *FFTokens) BurnTokens(ctx context.Context, operationID *fftypes.UUID, p From: burn.From, Amount: burn.Amount.Int().String(), RequestID: operationID.String(), - TrackingID: burn.TX.ID.String(), Operator: burn.Key, - Data: burn.MessageHash.String(), + Data: string(data), }). Post("/api/v1/burn") if err != nil || !res.IsSuccess() { @@ -383,6 +407,11 @@ func (ft *FFTokens) BurnTokens(ctx context.Context, operationID *fftypes.UUID, p } func (ft *FFTokens) TransferTokens(ctx context.Context, operationID *fftypes.UUID, poolProtocolID string, transfer *fftypes.TokenTransfer) error { + data, _ := json.Marshal(tokenData{ + TX: transfer.TX.ID, + Message: transfer.Message, + MessageHash: transfer.MessageHash, + }) res, err := ft.client.R().SetContext(ctx). SetBody(&transferTokens{ PoolID: poolProtocolID, @@ -391,9 +420,8 @@ func (ft *FFTokens) TransferTokens(ctx context.Context, operationID *fftypes.UUI To: transfer.To, Amount: transfer.Amount.Int().String(), RequestID: operationID.String(), - TrackingID: transfer.TX.ID.String(), Operator: transfer.Key, - Data: transfer.MessageHash.String(), + Data: string(data), }). Post("/api/v1/transfer") if err != nil || !res.IsSuccess() { diff --git a/internal/tokens/fftokens/fftokens_test.go b/internal/tokens/fftokens/fftokens_test.go index b6352ec3d0..0fd9193f3a 100644 --- a/internal/tokens/fftokens/fftokens_test.go +++ b/internal/tokens/fftokens/fftokens_test.go @@ -115,19 +115,19 @@ func TestCreateTokenPool(t *testing.T) { }, } - httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/pool", httpURL), + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/createpool", httpURL), func(req *http.Request) (*http.Response, error) { body := make(fftypes.JSONObject) err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ - "requestId": opID.String(), - "trackingId": pool.TX.ID.String(), - "operator": "0x123", - "type": "fungible", + "requestId": opID.String(), + "operator": "0x123", + "type": "fungible", "config": map[string]interface{}{ "foo": "bar", }, + "data": `{"tx":"` + pool.TX.ID.String() + `"}`, }, body) res := &http.Response{ @@ -156,13 +156,73 @@ func TestCreateTokenPoolError(t *testing.T) { }, } - httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/pool", httpURL), + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/createpool", httpURL), httpmock.NewJsonResponderOrPanic(500, fftypes.JSONObject{})) err := h.CreateTokenPool(context.Background(), fftypes.NewUUID(), pool) assert.Regexp(t, "FF10274", err) } +func TestActivateTokenPool(t *testing.T) { + h, _, _, httpURL, done := newTestFFTokens(t) + defer done() + + opID := fftypes.NewUUID() + pool := &fftypes.TokenPool{ + ProtocolID: "N1", + } + txInfo := map[string]interface{}{ + "foo": "bar", + } + tx := &fftypes.Transaction{ + Info: txInfo, + } + + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/activatepool", httpURL), + func(req *http.Request) (*http.Response, error) { + body := make(fftypes.JSONObject) + err := json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, fftypes.JSONObject{ + "requestId": opID.String(), + "poolId": "N1", + "transaction": txInfo, + }, body) + + res := &http.Response{ + Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + StatusCode: 202, + } + return res, nil + }) + + err := h.ActivateTokenPool(context.Background(), opID, pool, tx) + assert.NoError(t, err) +} + +func TestActivateTokenPoolError(t *testing.T) { + h, _, _, httpURL, done := newTestFFTokens(t) + defer done() + + pool := &fftypes.TokenPool{ + ID: fftypes.NewUUID(), + TX: fftypes.TransactionRef{ + ID: fftypes.NewUUID(), + Type: fftypes.TransactionTypeTokenPool, + }, + } + tx := &fftypes.Transaction{} + + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/activatepool", httpURL), + httpmock.NewJsonResponderOrPanic(500, fftypes.JSONObject{})) + + err := h.ActivateTokenPool(context.Background(), fftypes.NewUUID(), pool, tx) + assert.Regexp(t, "FF10274", err) +} + func TestMintTokens(t *testing.T) { h, _, _, httpURL, done := newTestFFTokens(t) defer done() @@ -185,12 +245,12 @@ func TestMintTokens(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ - "poolId": "123", - "to": "user1", - "amount": "10", - "operator": "0x123", - "requestId": opID.String(), - "trackingId": mint.TX.ID.String(), + "poolId": "123", + "to": "user1", + "amount": "10", + "operator": "0x123", + "requestId": opID.String(), + "data": `{"tx":"` + mint.TX.ID.String() + `"}`, }, body) res := &http.Response{ @@ -249,7 +309,7 @@ func TestBurnTokens(t *testing.T) { "amount": "10", "operator": "0x123", "requestId": opID.String(), - "trackingId": burn.TX.ID.String(), + "data": `{"tx":"` + burn.TX.ID.String() + `"}`, }, body) res := &http.Response{ @@ -310,7 +370,7 @@ func TestTransferTokens(t *testing.T) { "amount": "10", "operator": "0x123", "requestId": opID.String(), - "trackingId": transfer.TX.ID.String(), + "data": `{"tx":"` + transfer.TX.ID.String() + `"}`, }, body) res := &http.Response{ @@ -357,87 +417,241 @@ func TestEvents(t *testing.T) { opID := fftypes.NewUUID() txID := fftypes.NewUUID() - fromServer <- `{"id":"2","event":"receipt","data":{}}` - fromServer <- `{"id":"3","event":"receipt","data":{"id":"abc"}}` + fromServer <- fftypes.JSONObject{ + "id": "2", + "event": "receipt", + "data": fftypes.JSONObject{}, + }.String() + + fromServer <- fftypes.JSONObject{ + "id": "3", + "event": "receipt", + "data": fftypes.JSONObject{"id": "abc"}, + }.String() // receipt: success mcb.On("TokenOpUpdate", h, opID, fftypes.OpStatusSucceeded, "", mock.Anything).Return(nil).Once() - fromServer <- `{"id":"4","event":"receipt","data":{"id":"` + opID.String() + `","success":true}}` + fromServer <- fftypes.JSONObject{ + "id": "4", + "event": "receipt", + "data": fftypes.JSONObject{ + "id": opID.String(), + "success": true, + }, + }.String() // receipt: failure mcb.On("TokenOpUpdate", h, opID, fftypes.OpStatusFailed, "", mock.Anything).Return(nil).Once() - fromServer <- `{"id":"5","event":"receipt","data":{"id":"` + opID.String() + `","success":false}}` + fromServer <- fftypes.JSONObject{ + "id": "5", + "event": "receipt", + "data": fftypes.JSONObject{ + "id": opID.String(), + "success": false, + }, + }.String() // token-pool: missing data - fromServer <- `{"id":"6","event":"token-pool"}` + fromServer <- fftypes.JSONObject{ + "id": "6", + "event": "token-pool", + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"6"},"event":"ack"}`, string(msg)) - // token-pool: invalid uuid - fromServer <- `{"id":"7","event":"token-pool","data":{"trackingId":"bad","type":"fungible","poolId":"F1","operator":"0x0","transaction":{"transactionHash":"abc"}}}` + // token-pool: invalid uuid (success) + mcb.On("TokenPoolCreated", h, mock.MatchedBy(func(p *tokens.TokenPool) bool { + return p.ProtocolID == "F1" && p.Type == fftypes.TokenTypeFungible && p.Key == "0x0" && p.TransactionID == nil + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() + fromServer <- fftypes.JSONObject{ + "id": "7", + "event": "token-pool", + "data": fftypes.JSONObject{ + "type": "fungible", + "poolId": "F1", + "operator": "0x0", + "data": fftypes.JSONObject{"tx": "bad"}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"7"},"event":"ack"}`, string(msg)) // token-pool: success - mcb.On("TokenPoolCreated", h, mock.MatchedBy(func(p *fftypes.TokenPool) bool { - return p.ProtocolID == "F1" && p.Type == fftypes.TokenTypeFungible && p.Key == "0x0" && *p.TX.ID == *txID - }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil) - fromServer <- `{"id":"8","event":"token-pool","data":{"trackingId":"` + txID.String() + `","type":"fungible","poolId":"F1","operator":"0x0","transaction":{"transactionHash":"abc"}}}` + mcb.On("TokenPoolCreated", h, mock.MatchedBy(func(p *tokens.TokenPool) bool { + return p.ProtocolID == "F1" && p.Type == fftypes.TokenTypeFungible && p.Key == "0x0" && txID.Equals(p.TransactionID) + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() + fromServer <- fftypes.JSONObject{ + "id": "8", + "event": "token-pool", + "data": fftypes.JSONObject{ + "type": "fungible", + "poolId": "F1", + "operator": "0x0", + "data": fftypes.JSONObject{"tx": txID.String()}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"8"},"event":"ack"}`, string(msg)) // token-mint: missing data - fromServer <- `{"id":"9","event":"token-mint"}` + fromServer <- fftypes.JSONObject{ + "id": "9", + "event": "token-mint", + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"9"},"event":"ack"}`, string(msg)) // token-mint: invalid amount - fromServer <- `{"id":"10","event":"token-mint","data":{"poolId":"F1","tokenIndex":"0","operator":"0x0","to":"0x0","amount":"bad","trackingId":"` + txID.String() + `","transaction":{"transactionHash":"abc"}}}` + fromServer <- fftypes.JSONObject{ + "id": "10", + "event": "token-mint", + "data": fftypes.JSONObject{ + "poolId": "F1", + "tokenIndex": "0", + "operator": "0x0", + "to": "0x0", + "amount": "bad", + "data": fftypes.JSONObject{"tx": txID.String()}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"10"},"event":"ack"}`, string(msg)) // token-mint: success mcb.On("TokensTransferred", h, "F1", mock.MatchedBy(func(t *fftypes.TokenTransfer) bool { return t.Amount.Int().Int64() == 2 && t.To == "0x0" && t.TokenIndex == "" && *t.TX.ID == *txID - }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil) - fromServer <- `{"id":"11","event":"token-mint","data":{"poolId":"F1","operator":"0x0","to":"0x0","amount":"2","trackingId":"` + txID.String() + `","transaction":{"transactionHash":"abc"}}}` + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() + fromServer <- fftypes.JSONObject{ + "id": "11", + "event": "token-mint", + "data": fftypes.JSONObject{ + "poolId": "F1", + "operator": "0x0", + "to": "0x0", + "amount": "2", + "data": fftypes.JSONObject{"tx": txID.String()}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"11"},"event":"ack"}`, string(msg)) // token-mint: invalid uuid (success) mcb.On("TokensTransferred", h, "N1", mock.MatchedBy(func(t *fftypes.TokenTransfer) bool { return t.Amount.Int().Int64() == 1 && t.To == "0x0" && t.TokenIndex == "1" - }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil) - fromServer <- `{"id":"12","event":"token-mint","data":{"poolId":"N1","tokenIndex":"1","operator":"0x0","to":"0x0","amount":"1","trackingId":"bad","transaction":{"transactionHash":"abc"}}}` + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() + fromServer <- fftypes.JSONObject{ + "id": "12", + "event": "token-mint", + "data": fftypes.JSONObject{ + "poolId": "N1", + "tokenIndex": "1", + "operator": "0x0", + "to": "0x0", + "amount": "1", + "data": fftypes.JSONObject{"tx": "bad"}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"12"},"event":"ack"}`, string(msg)) // token-transfer: missing from - fromServer <- `{"id":"13","event":"token-transfer","data":{"poolId":"F1","tokenIndex":"0","operator":"0x0","to":"0x0","amount":"2","trackingId":"` + txID.String() + `","transaction":{"transactionHash":"abc"}}}` + fromServer <- fftypes.JSONObject{ + "id": "13", + "event": "token-transfer", + "data": fftypes.JSONObject{ + "poolId": "F1", + "tokenIndex": "0", + "operator": "0x0", + "to": "0x0", + "amount": "2", + "data": fftypes.JSONObject{"tx": txID.String()}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"13"},"event":"ack"}`, string(msg)) // token-transfer: bad message hash (success) mcb.On("TokensTransferred", h, "F1", mock.MatchedBy(func(t *fftypes.TokenTransfer) bool { return t.Amount.Int().Int64() == 2 && t.From == "0x0" && t.To == "0x1" && t.TokenIndex == "" - }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil) - fromServer <- `{"id":"14","event":"token-transfer","data":{"poolId":"F1","operator":"0x0","from":"0x0","to":"0x1","amount":"2","trackingId":"` + txID.String() + `","data":"bad","transaction":{"transactionHash":"abc"}}}` + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() + fromServer <- fftypes.JSONObject{ + "id": "14", + "event": "token-transfer", + "data": fftypes.JSONObject{ + "poolId": "F1", + "operator": "0x0", + "from": "0x0", + "to": "0x1", + "amount": "2", + "data": fftypes.JSONObject{"tx": txID.String(), "messageHash": "bad"}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"14"},"event":"ack"}`, string(msg)) // token-transfer: success + messageID := fftypes.NewUUID() mcb.On("TokensTransferred", h, "F1", mock.MatchedBy(func(t *fftypes.TokenTransfer) bool { - return t.Amount.Int().Int64() == 2 && t.From == "0x0" && t.To == "0x1" && t.TokenIndex == "" - }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil) - fromServer <- `{"id":"15","event":"token-transfer","data":{"poolId":"F1","operator":"0x0","from":"0x0","to":"0x1","amount":"2","trackingId":"` + txID.String() + `","transaction":{"transactionHash":"abc"}}}` + return t.Amount.Int().Int64() == 2 && t.From == "0x0" && t.To == "0x1" && t.TokenIndex == "" && messageID.Equals(t.Message) + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() + fromServer <- fftypes.JSONObject{ + "id": "15", + "event": "token-transfer", + "data": fftypes.JSONObject{ + "poolId": "F1", + "operator": "0x0", + "from": "0x0", + "to": "0x1", + "amount": "2", + "data": fftypes.JSONObject{"tx": txID.String(), "message": messageID.String()}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"15"},"event":"ack"}`, string(msg)) // token-burn: success mcb.On("TokensTransferred", h, "F1", mock.MatchedBy(func(t *fftypes.TokenTransfer) bool { return t.Amount.Int().Int64() == 2 && t.From == "0x0" && t.TokenIndex == "0" - }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil) - fromServer <- `{"id":"16","event":"token-burn","data":{"poolId":"F1","tokenIndex":"0","operator":"0x0","from":"0x0","amount":"2","trackingId":"` + txID.String() + `","transaction":{"transactionHash":"abc"}}}` + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() + fromServer <- fftypes.JSONObject{ + "id": "16", + "event": "token-burn", + "data": fftypes.JSONObject{ + "poolId": "F1", + "tokenIndex": "0", + "operator": "0x0", + "from": "0x0", + "amount": "2", + "data": fftypes.JSONObject{"tx": txID.String()}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"16"},"event":"ack"}`, string(msg)) diff --git a/internal/txcommon/token_inputs.go b/internal/txcommon/token_inputs.go index 4383c2248e..aba4413a42 100644 --- a/internal/txcommon/token_inputs.go +++ b/internal/txcommon/token_inputs.go @@ -18,10 +18,37 @@ package txcommon import ( "context" + "fmt" "github.com/hyperledger/firefly/pkg/fftypes" ) +func AddTokenPoolCreateInputs(op *fftypes.Operation, pool *fftypes.TokenPool) { + op.Input = fftypes.JSONObject{ + "id": pool.ID.String(), + "namespace": pool.Namespace, + "name": pool.Name, + "symbol": pool.Symbol, + "config": pool.Config, + } +} + +func RetrieveTokenPoolCreateInputs(ctx context.Context, op *fftypes.Operation, pool *fftypes.TokenPool) (err error) { + input := &op.Input + pool.ID, err = fftypes.ParseUUID(ctx, input.GetString("id")) + if err != nil { + return err + } + pool.Namespace = input.GetString("namespace") + pool.Name = input.GetString("name") + if pool.Namespace == "" || pool.Name == "" { + return fmt.Errorf("namespace or name missing from inputs") + } + pool.Symbol = input.GetString("symbol") + pool.Config = input.GetObject("config") + return nil +} + func AddTokenTransferInputs(op *fftypes.Operation, transfer *fftypes.TokenTransfer) { op.Input = fftypes.JSONObject{ "id": transfer.LocalID.String(), diff --git a/internal/txcommon/token_inputs_test.go b/internal/txcommon/token_inputs_test.go index d56fb61a8e..0a2e00e685 100644 --- a/internal/txcommon/token_inputs_test.go +++ b/internal/txcommon/token_inputs_test.go @@ -24,6 +24,77 @@ import ( "github.com/stretchr/testify/assert" ) +func TestAddTokenPoolCreateInputs(t *testing.T) { + op := &fftypes.Operation{} + config := fftypes.JSONObject{ + "foo": "bar", + } + pool := &fftypes.TokenPool{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + Name: "testpool", + Symbol: "FFT", + Config: config, + } + + AddTokenPoolCreateInputs(op, pool) + assert.Equal(t, pool.ID.String(), op.Input.GetString("id")) + assert.Equal(t, "ns1", op.Input.GetString("namespace")) + assert.Equal(t, "testpool", op.Input.GetString("name")) + assert.Equal(t, "FFT", op.Input.GetString("symbol")) + assert.Equal(t, pool.Config, op.Input.GetObject("config")) +} + +func TestRetrieveTokenPoolCreateInputs(t *testing.T) { + id := fftypes.NewUUID() + config := fftypes.JSONObject{ + "foo": "bar", + } + op := &fftypes.Operation{ + Input: fftypes.JSONObject{ + "id": id.String(), + "namespace": "ns1", + "name": "testpool", + "symbol": "FFT", + "config": config, + }, + } + pool := &fftypes.TokenPool{} + + err := RetrieveTokenPoolCreateInputs(context.Background(), op, pool) + assert.NoError(t, err) + assert.Equal(t, *id, *pool.ID) + assert.Equal(t, "ns1", pool.Namespace) + assert.Equal(t, "testpool", pool.Name) + assert.Equal(t, "FFT", pool.Symbol) + assert.Equal(t, config, pool.Config) +} + +func TestRetrieveTokenPoolCreateInputsBadID(t *testing.T) { + op := &fftypes.Operation{ + Input: fftypes.JSONObject{ + "id": "bad", + }, + } + pool := &fftypes.TokenPool{} + + err := RetrieveTokenPoolCreateInputs(context.Background(), op, pool) + assert.Regexp(t, "FF10142", err) +} + +func TestRetrieveTokenPoolCreateInputsNoName(t *testing.T) { + op := &fftypes.Operation{ + Input: fftypes.JSONObject{ + "id": fftypes.NewUUID().String(), + "namespace": "ns1", + }, + } + pool := &fftypes.TokenPool{} + + err := RetrieveTokenPoolCreateInputs(context.Background(), op, pool) + assert.Error(t, err) +} + func TestAddTokenTransferInputs(t *testing.T) { op := &fftypes.Operation{} transfer := &fftypes.TokenTransfer{ diff --git a/manifest.json b/manifest.json index 645826daf4..77aac8d70e 100644 --- a/manifest.json +++ b/manifest.json @@ -16,7 +16,7 @@ }, "tokens-erc1155": { "image": "ghcr.io/hyperledger/firefly-tokens-erc1155", - "tag": "v0.9.0-20211028-01", - "sha": "88d8f01e2c07dcdfd2706b51c7f591dba7072e72e168cec037549529355fdbbd" + "tag": "v0.9.0-20211119-7", + "sha": "ee512cb91d479c6f9424247c09eca4fdeb24a95c73d29a4ef5072d46819ca1f1" } } diff --git a/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index 845323844d..8d12fa2b94 100644 --- a/mocks/assetmocks/manager.go +++ b/mocks/assetmocks/manager.go @@ -11,8 +11,6 @@ import ( mock "github.com/stretchr/testify/mock" sysmessaging "github.com/hyperledger/firefly/internal/sysmessaging" - - tokens "github.com/hyperledger/firefly/pkg/tokens" ) // Manager is an autogenerated mock type for the Manager type @@ -20,6 +18,20 @@ type Manager struct { mock.Mock } +// ActivateTokenPool provides a mock function with given fields: ctx, pool, tx +func (_m *Manager) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, tx *fftypes.Transaction) error { + ret := _m.Called(ctx, pool, tx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.TokenPool, *fftypes.Transaction) error); ok { + r0 = rf(ctx, pool, tx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // BurnTokens provides a mock function with given fields: ctx, ns, transfer, waitConfirm func (_m *Manager) BurnTokens(ctx context.Context, ns string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (*fftypes.TokenTransfer, error) { ret := _m.Called(ctx, ns, transfer, waitConfirm) @@ -536,20 +548,6 @@ func (_m *Manager) Start() error { return r0 } -// TokenPoolCreated provides a mock function with given fields: ti, pool, protocolTxID, additionalInfo -func (_m *Manager) TokenPoolCreated(ti tokens.Plugin, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { - ret := _m.Called(ti, pool, protocolTxID, additionalInfo) - - var r0 error - if rf, ok := ret.Get(0).(func(tokens.Plugin, *fftypes.TokenPool, string, fftypes.JSONObject) error); ok { - r0 = rf(ti, pool, protocolTxID, additionalInfo) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // TransferTokens provides a mock function with given fields: ctx, ns, transfer, waitConfirm func (_m *Manager) TransferTokens(ctx context.Context, ns string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (*fftypes.TokenTransfer, error) { ret := _m.Called(ctx, ns, transfer, waitConfirm) @@ -596,20 +594,6 @@ func (_m *Manager) TransferTokensByType(ctx context.Context, ns string, connecto return r0, r1 } -// ValidateTokenPoolTx provides a mock function with given fields: ctx, pool, protocolTxID -func (_m *Manager) ValidateTokenPoolTx(ctx context.Context, pool *fftypes.TokenPool, protocolTxID string) error { - ret := _m.Called(ctx, pool, protocolTxID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.TokenPool, string) error); ok { - r0 = rf(ctx, pool, protocolTxID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // WaitStop provides a mock function with given fields: func (_m *Manager) WaitStop() { _m.Called() diff --git a/mocks/databasemocks/plugin.go b/mocks/databasemocks/plugin.go index ab23f31801..6c4bc9bc2f 100644 --- a/mocks/databasemocks/plugin.go +++ b/mocks/databasemocks/plugin.go @@ -1458,13 +1458,13 @@ func (_m *Plugin) GetTokenPoolByID(ctx context.Context, id *fftypes.UUID) (*ffty return r0, r1 } -// GetTokenPoolByProtocolID provides a mock function with given fields: ctx, id -func (_m *Plugin) GetTokenPoolByProtocolID(ctx context.Context, id string) (*fftypes.TokenPool, error) { - ret := _m.Called(ctx, id) +// GetTokenPoolByProtocolID provides a mock function with given fields: ctx, connector, protocolID +func (_m *Plugin) GetTokenPoolByProtocolID(ctx context.Context, connector string, protocolID string) (*fftypes.TokenPool, error) { + ret := _m.Called(ctx, connector, protocolID) var r0 *fftypes.TokenPool - if rf, ok := ret.Get(0).(func(context.Context, string) *fftypes.TokenPool); ok { - r0 = rf(ctx, id) + if rf, ok := ret.Get(0).(func(context.Context, string, string) *fftypes.TokenPool); ok { + r0 = rf(ctx, connector, protocolID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*fftypes.TokenPool) @@ -1472,8 +1472,8 @@ func (_m *Plugin) GetTokenPoolByProtocolID(ctx context.Context, id string) (*fft } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, id) + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, connector, protocolID) } else { r1 = ret.Error(1) } @@ -1536,6 +1536,29 @@ func (_m *Plugin) GetTokenTransfer(ctx context.Context, localID *fftypes.UUID) ( return r0, r1 } +// GetTokenTransferByProtocolID provides a mock function with given fields: ctx, connector, protocolID +func (_m *Plugin) GetTokenTransferByProtocolID(ctx context.Context, connector string, protocolID string) (*fftypes.TokenTransfer, error) { + ret := _m.Called(ctx, connector, protocolID) + + var r0 *fftypes.TokenTransfer + if rf, ok := ret.Get(0).(func(context.Context, string, string) *fftypes.TokenTransfer); ok { + r0 = rf(ctx, connector, protocolID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.TokenTransfer) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, connector, protocolID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetTokenTransfers provides a mock function with given fields: ctx, filter func (_m *Plugin) GetTokenTransfers(ctx context.Context, filter database.Filter) ([]*fftypes.TokenTransfer, *database.FilterResult, error) { ret := _m.Called(ctx, filter) diff --git a/mocks/eventmocks/event_manager.go b/mocks/eventmocks/event_manager.go index bb55e9b789..5922319ef9 100644 --- a/mocks/eventmocks/event_manager.go +++ b/mocks/eventmocks/event_manager.go @@ -231,13 +231,27 @@ func (_m *EventManager) SubscriptionUpdates() chan<- *fftypes.UUID { return r0 } -// TokensTransferred provides a mock function with given fields: tk, poolProtocolID, transfer, protocolTxID, additionalInfo -func (_m *EventManager) TokensTransferred(tk tokens.Plugin, poolProtocolID string, transfer *fftypes.TokenTransfer, protocolTxID string, additionalInfo fftypes.JSONObject) error { - ret := _m.Called(tk, poolProtocolID, transfer, protocolTxID, additionalInfo) +// TokenPoolCreated provides a mock function with given fields: ti, pool, protocolTxID, additionalInfo +func (_m *EventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { + ret := _m.Called(ti, pool, protocolTxID, additionalInfo) + + var r0 error + if rf, ok := ret.Get(0).(func(tokens.Plugin, *tokens.TokenPool, string, fftypes.JSONObject) error); ok { + r0 = rf(ti, pool, protocolTxID, additionalInfo) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TokensTransferred provides a mock function with given fields: ti, poolProtocolID, transfer, protocolTxID, additionalInfo +func (_m *EventManager) TokensTransferred(ti tokens.Plugin, poolProtocolID string, transfer *fftypes.TokenTransfer, protocolTxID string, additionalInfo fftypes.JSONObject) error { + ret := _m.Called(ti, poolProtocolID, transfer, protocolTxID, additionalInfo) var r0 error if rf, ok := ret.Get(0).(func(tokens.Plugin, string, *fftypes.TokenTransfer, string, fftypes.JSONObject) error); ok { - r0 = rf(tk, poolProtocolID, transfer, protocolTxID, additionalInfo) + r0 = rf(ti, poolProtocolID, transfer, protocolTxID, additionalInfo) } else { r0 = ret.Error(0) } diff --git a/mocks/syshandlersmocks/system_handlers.go b/mocks/syshandlersmocks/system_handlers.go index 12031e33ad..30f2986e5d 100644 --- a/mocks/syshandlersmocks/system_handlers.go +++ b/mocks/syshandlersmocks/system_handlers.go @@ -9,6 +9,8 @@ import ( fftypes "github.com/hyperledger/firefly/pkg/fftypes" mock "github.com/stretchr/testify/mock" + + syshandlers "github.com/hyperledger/firefly/internal/syshandlers" ) // SystemHandlers is an autogenerated mock type for the SystemHandlers type @@ -93,14 +95,14 @@ func (_m *SystemHandlers) GetGroups(ctx context.Context, filter database.AndFilt } // HandleSystemBroadcast provides a mock function with given fields: ctx, msg, data -func (_m *SystemHandlers) HandleSystemBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (bool, error) { +func (_m *SystemHandlers) HandleSystemBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (syshandlers.SystemBroadcastAction, error) { ret := _m.Called(ctx, msg, data) - var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Message, []*fftypes.Data) bool); ok { + var r0 syshandlers.SystemBroadcastAction + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.Message, []*fftypes.Data) syshandlers.SystemBroadcastAction); ok { r0 = rf(ctx, msg, data) } else { - r0 = ret.Get(0).(bool) + r0 = ret.Get(0).(syshandlers.SystemBroadcastAction) } var r1 error diff --git a/mocks/tokenmocks/callbacks.go b/mocks/tokenmocks/callbacks.go index 38ac494bc8..6df6200403 100644 --- a/mocks/tokenmocks/callbacks.go +++ b/mocks/tokenmocks/callbacks.go @@ -29,11 +29,11 @@ func (_m *Callbacks) TokenOpUpdate(plugin tokens.Plugin, operationID *fftypes.UU } // TokenPoolCreated provides a mock function with given fields: plugin, pool, protocolTxID, additionalInfo -func (_m *Callbacks) TokenPoolCreated(plugin tokens.Plugin, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { +func (_m *Callbacks) TokenPoolCreated(plugin tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { ret := _m.Called(plugin, pool, protocolTxID, additionalInfo) var r0 error - if rf, ok := ret.Get(0).(func(tokens.Plugin, *fftypes.TokenPool, string, fftypes.JSONObject) error); ok { + if rf, ok := ret.Get(0).(func(tokens.Plugin, *tokens.TokenPool, string, fftypes.JSONObject) error); ok { r0 = rf(plugin, pool, protocolTxID, additionalInfo) } else { r0 = ret.Error(0) diff --git a/mocks/tokenmocks/plugin.go b/mocks/tokenmocks/plugin.go index d5fb178640..bd8d1eb4e6 100644 --- a/mocks/tokenmocks/plugin.go +++ b/mocks/tokenmocks/plugin.go @@ -19,6 +19,20 @@ type Plugin struct { mock.Mock } +// ActivateTokenPool provides a mock function with given fields: ctx, operationID, pool, tx +func (_m *Plugin) ActivateTokenPool(ctx context.Context, operationID *fftypes.UUID, pool *fftypes.TokenPool, tx *fftypes.Transaction) error { + ret := _m.Called(ctx, operationID, pool, tx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, *fftypes.TokenPool, *fftypes.Transaction) error); ok { + r0 = rf(ctx, operationID, pool, tx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // BurnTokens provides a mock function with given fields: ctx, operationID, poolProtocolID, burn func (_m *Plugin) BurnTokens(ctx context.Context, operationID *fftypes.UUID, poolProtocolID string, burn *fftypes.TokenTransfer) error { ret := _m.Called(ctx, operationID, poolProtocolID, burn) diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go index 7c96f9465d..39bf051ae6 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -357,7 +357,7 @@ type iTokenPoolCollection interface { GetTokenPoolByID(ctx context.Context, id *fftypes.UUID) (*fftypes.TokenPool, error) // GetTokenPoolByID - Get a token pool by protocol ID - GetTokenPoolByProtocolID(ctx context.Context, id string) (*fftypes.TokenPool, error) + GetTokenPoolByProtocolID(ctx context.Context, connector, protocolID string) (*fftypes.TokenPool, error) // GetTokenPools - Get token pools GetTokenPools(ctx context.Context, filter Filter) ([]*fftypes.TokenPool, *FilterResult, error) @@ -387,6 +387,9 @@ type iTokenTransferCollection interface { // GetTokenTransfer - Get a token transfer by ID GetTokenTransfer(ctx context.Context, localID *fftypes.UUID) (*fftypes.TokenTransfer, error) + // GetTokenTransferByProtocolID - Get a token transfer by protocol ID + GetTokenTransferByProtocolID(ctx context.Context, connector, protocolID string) (*fftypes.TokenTransfer, error) + // GetTokenTransfers - Get token transfers GetTokenTransfers(ctx context.Context, filter Filter) ([]*fftypes.TokenTransfer, *FilterResult, error) } @@ -768,6 +771,7 @@ var TokenPoolQueryFactory = &queryFields{ "key": &StringField{}, "symbol": &StringField{}, "message": &UUIDField{}, + "state": &StringField{}, "created": &TimeField{}, "connector": &StringField{}, } @@ -795,6 +799,7 @@ var TokenTransferQueryFactory = &queryFields{ "to": &StringField{}, "amount": &Int64Field{}, "protocolid": &StringField{}, + "message": &UUIDField{}, "messagehash": &Bytes32Field{}, "created": &TimeField{}, } diff --git a/pkg/fftypes/tokenpool.go b/pkg/fftypes/tokenpool.go index 20137abbc8..cf531a4261 100644 --- a/pkg/fftypes/tokenpool.go +++ b/pkg/fftypes/tokenpool.go @@ -27,6 +27,19 @@ var ( TokenTypeNonFungible TokenType = ffEnum("tokentype", "nonfungible") ) +// TokenPoolState is the current confirmation state of a token pool +type TokenPoolState = FFEnum + +var ( + // TokenPoolStateUnknown is a token pool that may not yet be activated + // (should not be used in the code - only set via database migration for previously-created pools) + TokenPoolStateUnknown TokenPoolState = ffEnum("tokenpoolstate", "unknown") + // TokenPoolStatePending is a token pool that has been announced but not yet confirmed + TokenPoolStatePending TokenPoolState = ffEnum("tokenpoolstate", "pending") + // TokenPoolStateConfirmed is a token pool that has been confirmed on chain + TokenPoolStateConfirmed TokenPoolState = ffEnum("tokenpoolstate", "confirmed") +) + type TokenPool struct { ID *UUID `json:"id,omitempty"` Type TokenType `json:"type" ffenum:"tokentype"` @@ -38,17 +51,18 @@ type TokenPool struct { Symbol string `json:"symbol,omitempty"` Connector string `json:"connector,omitempty"` Message *UUID `json:"message,omitempty"` + State TokenPoolState `json:"state,omitempty" ffenum:"tokenpoolstate"` Created *FFTime `json:"created,omitempty"` - Config JSONObject `json:"config,omitempty"` + Config JSONObject `json:"config,omitempty"` // for REST calls only (not stored) TX TransactionRef `json:"tx,omitempty"` } type TokenPoolAnnouncement struct { - TokenPool - ProtocolTxID string `json:"protocolTxID"` + Pool *TokenPool `json:"pool"` + TX *Transaction `json:"tx"` } -func (t *TokenPool) Validate(ctx context.Context, existing bool) (err error) { +func (t *TokenPool) Validate(ctx context.Context) (err error) { if err = ValidateFFNameField(ctx, t.Namespace, "namespace"); err != nil { return err } @@ -58,10 +72,10 @@ func (t *TokenPool) Validate(ctx context.Context, existing bool) (err error) { return nil } -func (t *TokenPool) Topic() string { - return namespaceTopic(t.Namespace) +func (t *TokenPoolAnnouncement) Topic() string { + return namespaceTopic(t.Pool.Namespace) } -func (t *TokenPool) SetBroadcastMessage(msgID *UUID) { - t.Message = msgID +func (t *TokenPoolAnnouncement) SetBroadcastMessage(msgID *UUID) { + t.Pool.Message = msgID } diff --git a/pkg/fftypes/tokenpool_test.go b/pkg/fftypes/tokenpool_test.go index a21c568999..d700327265 100644 --- a/pkg/fftypes/tokenpool_test.go +++ b/pkg/fftypes/tokenpool_test.go @@ -28,21 +28,21 @@ func TestTokenPoolValidation(t *testing.T) { Namespace: "!wrong", Name: "ok", } - err := pool.Validate(context.Background(), false) + err := pool.Validate(context.Background()) assert.Regexp(t, "FF10131.*'namespace'", err) pool = &TokenPool{ Namespace: "ok", Name: "!wrong", } - err = pool.Validate(context.Background(), false) + err = pool.Validate(context.Background()) assert.Regexp(t, "FF10131.*'name'", err) pool = &TokenPool{ Namespace: "ok", Name: "ok", } - err = pool.Validate(context.Background(), false) + err = pool.Validate(context.Background()) assert.NoError(t, err) } @@ -51,7 +51,7 @@ func TestTokenPoolDefinition(t *testing.T) { Namespace: "ok", Name: "ok", } - var def Definition = pool + var def Definition = &TokenPoolAnnouncement{Pool: pool} assert.Equal(t, "ff_ns_ok", def.Topic()) id := NewUUID() diff --git a/pkg/fftypes/tokentransfer.go b/pkg/fftypes/tokentransfer.go index 1c47a62bbb..13a5e89bf0 100644 --- a/pkg/fftypes/tokentransfer.go +++ b/pkg/fftypes/tokentransfer.go @@ -36,6 +36,7 @@ type TokenTransfer struct { To string `json:"to,omitempty"` Amount BigInt `json:"amount"` ProtocolID string `json:"protocolId,omitempty"` + Message *UUID `json:"message,omitempty"` MessageHash *Bytes32 `json:"messageHash,omitempty"` Created *FFTime `json:"created,omitempty"` TX TransactionRef `json:"tx,omitempty"` diff --git a/pkg/tokens/plugin.go b/pkg/tokens/plugin.go index b4bdc0af6b..911b49f3e4 100644 --- a/pkg/tokens/plugin.go +++ b/pkg/tokens/plugin.go @@ -43,6 +43,9 @@ type Plugin interface { // CreateTokenPool creates a new (fungible or non-fungible) pool of tokens CreateTokenPool(ctx context.Context, operationID *fftypes.UUID, pool *fftypes.TokenPool) error + // ActivateTokenPool activates a pool in order to begin receiving events + ActivateTokenPool(ctx context.Context, operationID *fftypes.UUID, pool *fftypes.TokenPool, tx *fftypes.Transaction) error + // MintTokens mints new tokens in a pool and adds them to the recipient's account MintTokens(ctx context.Context, operationID *fftypes.UUID, poolProtocolID string, mint *fftypes.TokenTransfer) error @@ -72,7 +75,7 @@ type Callbacks interface { // submitted by us, or by any other authorized party in the network. // // Error should will only be returned in shutdown scenarios - TokenPoolCreated(plugin Plugin, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error + TokenPoolCreated(plugin Plugin, pool *TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error // TokensTransferred notifies on a transfer between token accounts. // @@ -84,3 +87,25 @@ type Callbacks interface { // interface implemented by the plugin, with the specified config type Capabilities struct { } + +// TokenPool is the set of data returned from the connector when a token pool is created. +type TokenPool struct { + // Type is the type of tokens (fungible, non-fungible, etc) in this pool + Type fftypes.TokenType + + // ProtocolID is the ID assigned to this pool by the connector (must be unique for this connector) + ProtocolID string + + // TransactionID is the FireFly-assigned ID to correlate this to a transaction (optional) + // Not guaranteed to be set for pool creation events triggered outside of FireFly + TransactionID *fftypes.UUID + + // Key is the chain-specific identifier for the user that created the pool + Key string + + // Connector is the configured name of this connector + Connector string + + // Standard is the well-defined token standard that this pool conforms to (optional) + Standard string +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 69c2a79be2..30aad9d22a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -233,6 +233,17 @@ func beforeE2ETest(t *testing.T) *testState { return ts } +func waitForMessageConfirmed(t *testing.T, c chan *fftypes.EventDelivery, msgType fftypes.MessageType) *fftypes.EventDelivery { + for { + ed := <-c + if ed.Type == fftypes.EventTypeMessageConfirmed && ed.Message != nil && ed.Message.Header.Type == msgType { + t.Logf("Detected '%s' event for message '%s' of type '%s'", ed.Type, ed.Message.Header.ID, msgType) + return ed + } + t.Logf("Ignored event '%s'", ed.ID) + } +} + func wsReader(t *testing.T, conn *websocket.Conn) (chan *fftypes.EventDelivery, chan *fftypes.ChangeEvent) { events := make(chan *fftypes.EventDelivery, 100) changeEvents := make(chan *fftypes.ChangeEvent, 100) diff --git a/test/e2e/onchain_offchain_test.go b/test/e2e/onchain_offchain_test.go index 098a1c92ec..3dec6d0601 100644 --- a/test/e2e/onchain_offchain_test.go +++ b/test/e2e/onchain_offchain_test.go @@ -59,12 +59,12 @@ func (suite *OnChainOffChainTestSuite) TestE2EBroadcast() { require.NoError(suite.T(), err) assert.Equal(suite.T(), 202, resp.StatusCode()) - <-received1 + waitForMessageConfirmed(suite.T(), received1, fftypes.MessageTypeBroadcast) <-changes1 // also expect database change events val1 := validateReceivedMessages(suite.testState, suite.testState.client1, fftypes.MessageTypeBroadcast, fftypes.TransactionTypeBatchPin, 1, 0) assert.Equal(suite.T(), data.Value, val1) - <-received2 + waitForMessageConfirmed(suite.T(), received2, fftypes.MessageTypeBroadcast) <-changes2 // also expect database change events val2 := validateReceivedMessages(suite.testState, suite.testState.client2, fftypes.MessageTypeBroadcast, fftypes.TransactionTypeBatchPin, 1, 0) assert.Equal(suite.T(), data.Value, val2) @@ -116,9 +116,9 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesBroadcast() { require.NoError(suite.T(), err) assert.Equal(suite.T(), 200, resp.StatusCode()) - <-received1 + waitForMessageConfirmed(suite.T(), received1, fftypes.MessageTypeBroadcast) <-changes1 // also expect database change events - <-received2 + waitForMessageConfirmed(suite.T(), received2, fftypes.MessageTypeBroadcast) <-changes2 // also expect database change events } @@ -176,9 +176,9 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesPrivate() { require.NoError(suite.T(), err) assert.Equal(suite.T(), 200, resp.StatusCode()) - <-received1 + waitForMessageConfirmed(suite.T(), received1, fftypes.MessageTypePrivate) <-changes1 // also expect database change events - <-received2 + waitForMessageConfirmed(suite.T(), received2, fftypes.MessageTypePrivate) <-changes2 // also expect database change events } @@ -222,11 +222,11 @@ func (suite *OnChainOffChainTestSuite) TestE2EBroadcastBlob() { require.NoError(suite.T(), err) assert.Equal(suite.T(), 202, resp.StatusCode()) - <-received1 + waitForMessageConfirmed(suite.T(), received1, fftypes.MessageTypeBroadcast) val1 := validateReceivedMessages(suite.testState, suite.testState.client1, fftypes.MessageTypeBroadcast, fftypes.TransactionTypeBatchPin, 1, 0) assert.Regexp(suite.T(), "myfile.txt", string(val1)) - <-received2 + waitForMessageConfirmed(suite.T(), received2, fftypes.MessageTypeBroadcast) val2 := validateReceivedMessages(suite.testState, suite.testState.client2, fftypes.MessageTypeBroadcast, fftypes.TransactionTypeBatchPin, 1, 0) assert.Regexp(suite.T(), "myfile.txt", string(val2)) @@ -247,10 +247,10 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivateBlobDatatypeTagged() { require.NoError(suite.T(), err) assert.Equal(suite.T(), 202, resp.StatusCode()) - <-received1 + waitForMessageConfirmed(suite.T(), received1, fftypes.MessageTypePrivate) _ = validateReceivedMessages(suite.testState, suite.testState.client1, fftypes.MessageTypePrivate, fftypes.TransactionTypeBatchPin, 1, 0) - <-received2 + waitForMessageConfirmed(suite.T(), received2, fftypes.MessageTypePrivate) _ = validateReceivedMessages(suite.testState, suite.testState.client2, fftypes.MessageTypePrivate, fftypes.TransactionTypeBatchPin, 1, 0) } @@ -291,17 +291,17 @@ func (suite *OnChainOffChainTestSuite) TestE2EWebhookExchange() { require.NoError(suite.T(), err) assert.Equal(suite.T(), 202, resp.StatusCode()) - <-received1 // request - <-received2 // request + waitForMessageConfirmed(suite.T(), received1, fftypes.MessageTypePrivate) // request 1 + waitForMessageConfirmed(suite.T(), received2, fftypes.MessageTypePrivate) // request 2 - <-received1 // reply + waitForMessageConfirmed(suite.T(), received1, fftypes.MessageTypePrivate) // reply 1 val1 := validateReceivedMessages(suite.testState, suite.testState.client1, fftypes.MessageTypePrivate, fftypes.TransactionTypeBatchPin, 2, 0) assert.Equal(suite.T(), float64(200), val1.JSONObject()["status"]) decoded1, err := base64.StdEncoding.DecodeString(val1.JSONObject().GetString("body")) assert.NoError(suite.T(), err) assert.Regexp(suite.T(), "Example YAML", string(decoded1)) - <-received2 // reply + waitForMessageConfirmed(suite.T(), received2, fftypes.MessageTypePrivate) // reply 2 val2 := validateReceivedMessages(suite.testState, suite.testState.client1, fftypes.MessageTypePrivate, fftypes.TransactionTypeBatchPin, 2, 0) assert.Equal(suite.T(), float64(200), val2.JSONObject()["status"]) decoded2, err := base64.StdEncoding.DecodeString(val2.JSONObject().GetString("body")) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index a6bcb0ccd7..680b42322c 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -67,7 +67,7 @@ if [ "$BUILD_FIREFLY" == "true" ]; then fi if [ "$DOWNLOAD_CLI" == "true" ]; then - go install github.com/hyperledger/firefly-cli/ff@v0.0.37 + go install github.com/hyperledger/firefly-cli/ff@v0.0.39 checkOk $? fi