From 9bb884daa8964eca22de97a1eeed483576ed8664 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 9 Nov 2021 16:51:52 -0500 Subject: [PATCH 01/13] Pass all extra info to tokens plugin in "data" argument The standalone "trackingId" parameter is being removed. Also differentiate between the stored TokenPool type in FireFly core and the simpler TokenPool data that is returned from the tokens plugin. Signed-off-by: Andrew Richardson --- internal/assets/manager.go | 2 +- internal/assets/token_pool_created.go | 46 +-- internal/assets/token_pool_created_test.go | 68 ++--- internal/broadcast/tokenpool.go | 6 +- internal/broadcast/tokenpool_test.go | 8 +- internal/orchestrator/bound_callbacks.go | 2 +- internal/orchestrator/bound_callbacks_test.go | 3 +- internal/syshandlers/syshandler_tokenpool.go | 41 +-- .../syshandlers/syshandler_tokenpool_test.go | 261 +++++++++--------- internal/tokens/fftokens/fftokens.go | 127 ++++----- internal/tokens/fftokens/fftokens_test.go | 212 +++++++++++--- mocks/assetmocks/manager.go | 4 +- mocks/tokenmocks/callbacks.go | 4 +- pkg/fftypes/tokenpool.go | 12 +- pkg/fftypes/tokenpool_test.go | 2 +- pkg/tokens/plugin.go | 12 +- 16 files changed, 492 insertions(+), 318 deletions(-) diff --git a/internal/assets/manager.go b/internal/assets/manager.go index b5a6a4e9fa..21f904268f 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -56,7 +56,7 @@ 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 + TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error // Deprecated CreateTokenPoolByType(ctx context.Context, ns, connector string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) diff --git a/internal/assets/token_pool_created.go b/internal/assets/token_pool_created.go index 34309318f8..c0ba11d4ee 100644 --- a/internal/assets/token_pool_created.go +++ b/internal/assets/token_pool_created.go @@ -25,31 +25,41 @@ import ( "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, +func (am *assetManager) updatePool(storedPool *fftypes.TokenPool, chainPool *tokens.TokenPool) *fftypes.TokenPool { + storedPool.Type = chainPool.Type + storedPool.ProtocolID = chainPool.ProtocolID + storedPool.Key = chainPool.Key + storedPool.Connector = chainPool.Connector + storedPool.Standard = chainPool.Standard + storedPool.TX = fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: chainPool.TransactionID, } - pool = &announce.TokenPool + return storedPool +} +func (am *assetManager) TokenPoolCreated(tk tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { + var newPool *fftypes.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("tx", pool.TransactionID), 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) + log.L(ctx).Debugf("Token pool transaction '%s' ignored, as it did not match an operation submitted by this node", pool.TransactionID) return nil } - err = retrieveTokenPoolCreateInputs(ctx, operations[0], pool) + newPool = am.updatePool(&fftypes.TokenPool{}, pool) + err = retrieveTokenPoolCreateInputs(ctx, operations[0], newPool) if err != nil { - log.L(ctx).Errorf("Error retrieving pool info from transaction '%s' (%s) - ignoring: %v", pool.TX.ID, err, operations[0].Input) + log.L(ctx).Errorf("Error retrieving pool info from transaction '%s' (%s) - ignoring: %v", pool.TransactionID, err, operations[0].Input) return nil } @@ -59,13 +69,13 @@ func (am *assetManager) TokenPoolCreated(tk tokens.Plugin, pool *fftypes.TokenPo // 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, + ID: newPool.TX.ID, Status: fftypes.OpStatusPending, Subject: fftypes.TransactionSubject{ - Namespace: pool.Namespace, - Type: pool.TX.Type, - Signer: pool.Key, - Reference: pool.ID, + Namespace: newPool.Namespace, + Type: newPool.TX.Type, + Signer: newPool.Key, + Reference: newPool.ID, }, ProtocolID: protocolTxID, Info: additionalInfo, @@ -74,7 +84,7 @@ func (am *assetManager) TokenPoolCreated(tk tokens.Plugin, pool *fftypes.TokenPo // Add a new operation for the announcement op := fftypes.NewTXOperation( tk, - pool.Namespace, + newPool.Namespace, transaction.ID, "", fftypes.OpTypeTokenAnnouncePool, @@ -94,6 +104,10 @@ func (am *assetManager) TokenPoolCreated(tk tokens.Plugin, pool *fftypes.TokenPo } // Announce the details of the new token pool - _, err = am.broadcast.BroadcastTokenPool(am.ctx, pool.Namespace, announce, false) + announce := &fftypes.TokenPoolAnnouncement{ + Pool: newPool, + ProtocolTxID: protocolTxID, + } + _, err = am.broadcast.BroadcastTokenPool(am.ctx, newPool.Namespace, announce, false) return err } diff --git a/internal/assets/token_pool_created_test.go b/internal/assets/token_pool_created_test.go index 0eee43e12f..dfb5e2ea04 100644 --- a/internal/assets/token_pool_created_test.go +++ b/internal/assets/token_pool_created_test.go @@ -24,6 +24,7 @@ import ( "github.com/hyperledger/firefly/mocks/tokenmocks" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/hyperledger/firefly/pkg/tokens" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -47,14 +48,11 @@ func TestTokenPoolCreatedSuccess(t *testing.T) { }, }, } - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, } mti.On("Name").Return("mock-tokens") @@ -67,7 +65,7 @@ func TestTokenPoolCreatedSuccess(t *testing.T) { 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 + return pool.Pool.Namespace == "test-ns" && pool.Pool.Name == "my-pool" && *pool.Pool.ID == *poolID }), false).Return(nil, nil) info := fftypes.JSONObject{"some": "info"} @@ -87,14 +85,11 @@ func TestTokenPoolCreatedOpNotFound(t *testing.T) { txID := fftypes.NewUUID() operations := []*fftypes.Operation{} - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, } mti.On("Name").Return("mock-tokens") @@ -122,14 +117,11 @@ func TestTokenPoolMissingID(t *testing.T) { Input: fftypes.JSONObject{}, }, } - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, } mti.On("Name").Return("mock-tokens") @@ -160,14 +152,11 @@ func TestTokenPoolCreatedMissingNamespace(t *testing.T) { }, }, } - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, } mti.On("Name").Return("mock-tokens") @@ -200,14 +189,11 @@ func TestTokenPoolCreatedUpsertFail(t *testing.T) { }, }, } - pool := &fftypes.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TX: fftypes.TransactionRef{ - ID: txID, - Type: fftypes.TransactionTypeTokenPool, - }, + pool := &tokens.TokenPool{ + Type: fftypes.TokenTypeFungible, + ProtocolID: "123", + Key: "0x0", + TransactionID: txID, } mti.On("Name").Return("mock-tokens") diff --git a/internal/broadcast/tokenpool.go b/internal/broadcast/tokenpool.go index da77ea813d..2606dee647 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, false); 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..5cb3853be3 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", @@ -61,7 +61,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: "", @@ -87,7 +87,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", @@ -119,7 +119,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", diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go index 104fe226a5..e802c88011 100644 --- a/internal/orchestrator/bound_callbacks.go +++ b/internal/orchestrator/bound_callbacks.go @@ -56,7 +56,7 @@ 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 { +func (bc *boundCallbacks) TokenPoolCreated(plugin tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { return bc.am.TokenPoolCreated(plugin, pool, protocolTxID, additionalInfo) } diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go index c34eb9a805..d18ae54242 100644 --- a/internal/orchestrator/bound_callbacks_test.go +++ b/internal/orchestrator/bound_callbacks_test.go @@ -27,6 +27,7 @@ import ( "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" ) @@ -40,7 +41,7 @@ func TestBoundCallbacks(t *testing.T) { 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() diff --git a/internal/syshandlers/syshandler_tokenpool.go b/internal/syshandlers/syshandler_tokenpool.go index 8942e8562c..a35875c3f4 100644 --- a/internal/syshandlers/syshandler_tokenpool.go +++ b/internal/syshandlers/syshandler_tokenpool.go @@ -24,7 +24,9 @@ import ( "github.com/hyperledger/firefly/pkg/fftypes" ) -func (sh *systemHandlers) persistTokenPool(ctx context.Context, pool *fftypes.TokenPoolAnnouncement) (valid bool, err error) { +func (sh *systemHandlers) persistTokenPool(ctx context.Context, announce *fftypes.TokenPoolAnnouncement) (valid bool, err error) { + pool := announce.Pool + // Find a matching operation within this transaction fb := database.OperationQueryFactory.NewFilter(ctx) filter := fb.And( @@ -50,8 +52,8 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, pool *fftypes.To 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) + if transaction.ProtocolID != announce.ProtocolTxID { + log.L(ctx).Warnf("Ignoring token pool from transaction '%s' - unexpected protocol ID '%s'", pool.TX.ID, announce.ProtocolTxID) return false, nil // not retryable } @@ -63,8 +65,8 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, pool *fftypes.To } } 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) + log.L(ctx).Infof("Validating token pool transaction '%s' with protocol ID '%s'", pool.TX.ID, announce.ProtocolTxID) + err = sh.assets.ValidateTokenPoolTx(ctx, pool, announce.ProtocolTxID) if err != nil { log.L(ctx).Errorf("Failed to validate token pool transaction '%s': %v", pool.TX.ID, err) return false, err // retryable @@ -78,7 +80,7 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, pool *fftypes.To Signer: pool.Key, Reference: pool.ID, }, - ProtocolID: pool.ProtocolTxID, + ProtocolID: announce.ProtocolTxID, } valid, err = sh.txhelper.PersistTransaction(ctx, transaction) if !valid || err != nil { @@ -86,7 +88,7 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, pool *fftypes.To } } - err = sh.database.UpsertTokenPool(ctx, &pool.TokenPool) + 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) @@ -101,18 +103,19 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, pool *fftypes.To 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 - } + var announce fftypes.TokenPoolAnnouncement + if valid = sh.getSystemBroadcastPayload(ctx, msg, data, &announce); !valid { + l.Errorf("Unable to process token pool broadcast %s - message malformed: %s", msg.Header.ID, err) + return false, nil + } + pool := announce.Pool + 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 { + announce.Pool.Message = msg.Header.ID + if valid, err = sh.persistTokenPool(ctx, &announce); err != nil { + return valid, err } } diff --git a/internal/syshandlers/syshandler_tokenpool_test.go b/internal/syshandlers/syshandler_tokenpool_test.go index b316996d4d..e8e2f5d43d 100644 --- a/internal/syshandlers/syshandler_tokenpool_test.go +++ b/internal/syshandlers/syshandler_tokenpool_test.go @@ -33,19 +33,20 @@ import ( 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(), - }, + 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(), }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -54,7 +55,7 @@ func TestHandleSystemBroadcastTokenPoolSelfOk(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -86,8 +87,8 @@ func TestHandleSystemBroadcastTokenPoolSelfOk(t *testing.T) { func TestHandleSystemBroadcastTokenPoolSelfUpdateOpFail(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ + announce := &fftypes.TokenPoolAnnouncement{ + Pool: &fftypes.TokenPool{ ID: fftypes.NewUUID(), Namespace: "ns1", Name: "name1", @@ -107,7 +108,7 @@ func TestHandleSystemBroadcastTokenPoolSelfUpdateOpFail(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -129,19 +130,20 @@ func TestHandleSystemBroadcastTokenPoolSelfUpdateOpFail(t *testing.T) { func TestHandleSystemBroadcastTokenPoolSelfGetTXFail(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(), - }, + 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(), }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -150,7 +152,7 @@ func TestHandleSystemBroadcastTokenPoolSelfGetTXFail(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -173,19 +175,20 @@ func TestHandleSystemBroadcastTokenPoolSelfGetTXFail(t *testing.T) { func TestHandleSystemBroadcastTokenPoolSelfTXMismatch(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(), - }, + 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(), }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -194,7 +197,7 @@ func TestHandleSystemBroadcastTokenPoolSelfTXMismatch(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -221,19 +224,20 @@ func TestHandleSystemBroadcastTokenPoolSelfTXMismatch(t *testing.T) { func TestHandleSystemBroadcastTokenPoolSelfUpdateTXFail(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(), - }, + 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(), }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -242,7 +246,7 @@ func TestHandleSystemBroadcastTokenPoolSelfUpdateTXFail(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -267,19 +271,20 @@ func TestHandleSystemBroadcastTokenPoolSelfUpdateTXFail(t *testing.T) { func TestHandleSystemBroadcastTokenPoolOk(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(), - }, + 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(), }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -288,7 +293,7 @@ func TestHandleSystemBroadcastTokenPoolOk(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -324,19 +329,20 @@ func TestHandleSystemBroadcastTokenPoolOk(t *testing.T) { func TestHandleSystemBroadcastTokenPoolValidateTxFail(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(), - }, + 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(), }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -345,7 +351,7 @@ func TestHandleSystemBroadcastTokenPoolValidateTxFail(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -371,19 +377,20 @@ func TestHandleSystemBroadcastTokenPoolValidateTxFail(t *testing.T) { func TestHandleSystemBroadcastTokenPoolBadTX(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, - }, + pool := &fftypes.TokenPool{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + Name: "name1", + Type: fftypes.TokenTypeFungible, + ProtocolID: "12345", + Symbol: "COIN", + TX: fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: nil, }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -392,7 +399,7 @@ func TestHandleSystemBroadcastTokenPoolBadTX(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -421,19 +428,20 @@ func TestHandleSystemBroadcastTokenPoolBadTX(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(), - }, + 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(), }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -442,7 +450,7 @@ func TestHandleSystemBroadcastTokenPoolIDMismatch(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -478,19 +486,20 @@ func TestHandleSystemBroadcastTokenPoolIDMismatch(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(), - }, + 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(), }, + } + announce := &fftypes.TokenPoolAnnouncement{ + Pool: pool, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -499,7 +508,7 @@ func TestHandleSystemBroadcastTokenPoolFailUpsert(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -532,8 +541,8 @@ func TestHandleSystemBroadcastTokenPoolFailUpsert(t *testing.T) { func TestHandleSystemBroadcastTokenPoolOpsFail(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{ + announce := &fftypes.TokenPoolAnnouncement{ + Pool: &fftypes.TokenPool{ ID: fftypes.NewUUID(), Namespace: "ns1", Name: "name1", @@ -553,7 +562,7 @@ func TestHandleSystemBroadcastTokenPoolOpsFail(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), @@ -572,8 +581,8 @@ func TestHandleSystemBroadcastTokenPoolOpsFail(t *testing.T) { func TestHandleSystemBroadcastTokenPoolValidateFail(t *testing.T) { sh := newTestSystemHandlers(t) - pool := &fftypes.TokenPoolAnnouncement{ - TokenPool: fftypes.TokenPool{}, + announce := &fftypes.TokenPoolAnnouncement{ + Pool: &fftypes.TokenPool{}, ProtocolTxID: "tx123", } msg := &fftypes.Message{ @@ -582,7 +591,7 @@ func TestHandleSystemBroadcastTokenPoolValidateFail(t *testing.T) { Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&pool) + b, err := json.Marshal(&announce) assert.NoError(t, err) data := []*fftypes.Data{{ Value: fftypes.Byteable(b), diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go index 7ce8d3f716..7a3530356b 100644 --- a/internal/tokens/fftokens/fftokens.go +++ b/internal/tokens/fftokens/fftokens.go @@ -56,22 +56,26 @@ const ( messageTokenTransfer msgType = "token-transfer" ) +type tokenData struct { + TX *fftypes.UUID `json:"tx,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 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 +84,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 +95,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 +166,33 @@ 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") + poolDataString := data.GetString("data") tx := data.GetObject("transaction") txHash := tx.GetString("transactionHash") if tokenType == "" || protocolID == "" || - trackingID == "" || + poolDataString == "" || 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) + var poolData tokenData + if err = json.Unmarshal([]byte(poolDataString), &poolData); err != nil { + log.L(ctx).Errorf("TokenPool event is not valid - failed to parse data (%s): %+v", err, data) return nil // move on } - 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 @@ -231,21 +230,11 @@ func (ft *FFTokens) handleTokenTransfer(ctx context.Context, t fftypes.TokenTran } // 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) - } + // 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) } transfer := &fftypes.TokenTransfer{ @@ -256,9 +245,9 @@ func (ft *FFTokens) handleTokenTransfer(ctx context.Context, t fftypes.TokenTran To: toAddress, ProtocolID: txHash, Key: operatorAddress, - MessageHash: &messageHash, + MessageHash: transferData.MessageHash, TX: fftypes.TransactionRef{ - ID: txID, + ID: transferData.TX, Type: fftypes.TransactionTypeTokenTransfer, }, } @@ -330,15 +319,18 @@ 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/pool") + Post("/api/v1/createpool") if err != nil || !res.IsSuccess() { return restclient.WrapRestErr(ctx, res, err, i18n.MsgTokensRESTErr) } @@ -346,15 +338,18 @@ 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, + 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 +359,10 @@ 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, + MessageHash: burn.MessageHash, + }) res, err := ft.client.R().SetContext(ctx). SetBody(&burnTokens{ PoolID: poolProtocolID, @@ -371,9 +370,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 +381,10 @@ 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, + MessageHash: transfer.MessageHash, + }) res, err := ft.client.R().SetContext(ctx). SetBody(&transferTokens{ PoolID: poolProtocolID, @@ -391,9 +393,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..631b79e382 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,7 +156,7 @@ 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) @@ -185,12 +185,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 +249,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 +310,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,42 +357,109 @@ 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"}}}` + 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 + mcb.On("TokenPoolCreated", h, mock.MatchedBy(func(p *tokens.TokenPool) bool { + return p.ProtocolID == "F1" && p.Type == fftypes.TokenTypeFungible && p.Key == "0x0" && *p.TransactionID == *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"}}}` + 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)) @@ -400,7 +467,20 @@ func TestEvents(t *testing.T) { 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"}}}` + 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)) @@ -408,12 +488,40 @@ func TestEvents(t *testing.T) { 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"}}}` + 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)) @@ -421,7 +529,21 @@ func TestEvents(t *testing.T) { 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"}}}` + 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)) @@ -429,7 +551,21 @@ func TestEvents(t *testing.T) { 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"}}}` + 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()}.String(), + "transaction": fftypes.JSONObject{ + "transactionHash": "abc", + }, + }, + }.String() msg = <-toServer assert.Equal(t, `{"data":{"id":"15"},"event":"ack"}`, string(msg)) @@ -437,7 +573,21 @@ func TestEvents(t *testing.T) { 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"}}}` + 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/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index 845323844d..a8d9b97c24 100644 --- a/mocks/assetmocks/manager.go +++ b/mocks/assetmocks/manager.go @@ -537,11 +537,11 @@ func (_m *Manager) Start() error { } // 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 { +func (_m *Manager) 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, *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(ti, pool, protocolTxID, additionalInfo) } else { r0 = ret.Error(0) 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/pkg/fftypes/tokenpool.go b/pkg/fftypes/tokenpool.go index 20137abbc8..4a4945a480 100644 --- a/pkg/fftypes/tokenpool.go +++ b/pkg/fftypes/tokenpool.go @@ -44,8 +44,8 @@ type TokenPool struct { } type TokenPoolAnnouncement struct { - TokenPool - ProtocolTxID string `json:"protocolTxID"` + Pool *TokenPool `json:"pool"` + ProtocolTxID string `json:"protocolTxID"` } func (t *TokenPool) Validate(ctx context.Context, existing bool) (err error) { @@ -58,10 +58,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..710d4de287 100644 --- a/pkg/fftypes/tokenpool_test.go +++ b/pkg/fftypes/tokenpool_test.go @@ -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/tokens/plugin.go b/pkg/tokens/plugin.go index b4bdc0af6b..53904b58d2 100644 --- a/pkg/tokens/plugin.go +++ b/pkg/tokens/plugin.go @@ -72,7 +72,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 +84,13 @@ 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 fftypes.TokenType + ProtocolID string + TransactionID *fftypes.UUID + Key string + Connector string + Standard string +} From d07cfb6257ad109573f07209f9f964fdef640257 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 10 Nov 2021 13:55:43 -0500 Subject: [PATCH 02/13] Replace token pool "validate tx" logic stubs with a new "activate" action A pool must be activated before using it. The activation step will trigger the pool creation event to be re-sent (so that it can be used to confirm the pool), and will also trigger the plugin to begin sending transfers from that specific pool. This solves a number of holes with token pool creation - including validating the pool announcement against the local plugin and ensuring transfers are not received before the pool is fully created. Signed-off-by: Andrew Richardson --- .../000028_add_tokenpool_fields.down.sql | 3 + .../000028_add_tokenpool_fields.up.sql | 3 + .../000044_add_tokenpool_state.down.sql | 3 + .../000044_add_tokenpool_state.up.sql | 5 + .../000028_add_tokenpool_fields.down.sql | 3 + .../sqlite/000028_add_tokenpool_fields.up.sql | 3 + .../000044_add_tokenpool_state.down.sql | 1 + .../sqlite/000044_add_tokenpool_state.up.sql | 2 + docs/swagger/swagger.yaml | 26 + internal/apiserver/route_post_token_pool.go | 2 +- .../route_post_token_pool_by_type.go | 2 +- internal/assets/manager.go | 2 +- internal/assets/token_pool.go | 18 +- internal/assets/token_pool_created.go | 154 +++--- internal/assets/token_pool_created_test.go | 230 ++++++--- internal/assets/token_pool_test.go | 61 ++- internal/broadcast/tokenpool.go | 2 +- internal/broadcast/tokenpool_test.go | 4 - internal/database/sqlcommon/tokenpool_sql.go | 11 +- .../database/sqlcommon/tokenpool_sql_test.go | 3 +- internal/events/tokens_transferred.go | 2 +- internal/events/tokens_transferred_test.go | 13 +- internal/syshandlers/syshandler_tokenpool.go | 115 ++--- .../syshandlers/syshandler_tokenpool_test.go | 481 ++++-------------- internal/tokens/fftokens/fftokens.go | 20 + internal/tokens/fftokens/fftokens_test.go | 60 +++ mocks/assetmocks/manager.go | 28 +- mocks/databasemocks/plugin.go | 14 +- mocks/tokenmocks/plugin.go | 14 + pkg/database/plugin.go | 3 +- pkg/fftypes/tokenpool.go | 22 +- pkg/fftypes/tokenpool_test.go | 6 +- pkg/tokens/plugin.go | 3 + 33 files changed, 671 insertions(+), 648 deletions(-) create mode 100644 db/migrations/postgres/000044_add_tokenpool_state.down.sql create mode 100644 db/migrations/postgres/000044_add_tokenpool_state.up.sql create mode 100644 db/migrations/sqlite/000044_add_tokenpool_state.down.sql create mode 100644 db/migrations/sqlite/000044_add_tokenpool_state.up.sql 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/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/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/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/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 45b8ba2b75..947505203d 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: @@ -6238,6 +6251,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 +6325,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -6391,6 +6411,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -6426,6 +6448,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: @@ -6489,6 +6513,8 @@ paths: type: string standard: type: string + state: + type: string symbol: type: string tx: 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 21f904268f..9cc49017db 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) diff --git a/internal/assets/token_pool.go b/internal/assets/token_pool.go index 594d599460..c52614bde1 100644 --- a/internal/assets/token_pool.go +++ b/internal/assets/token_pool.go @@ -30,6 +30,7 @@ func addTokenPoolCreateInputs(op *fftypes.Operation, pool *fftypes.TokenPool) { "id": pool.ID.String(), "namespace": pool.Namespace, "name": pool.Name, + "symbol": pool.Symbol, "config": pool.Config, } } @@ -45,6 +46,7 @@ func retrieveTokenPoolCreateInputs(ctx context.Context, op *fftypes.Operation, p 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 } @@ -146,6 +148,17 @@ 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 { + if err := am.data.VerifyNamespaceExists(ctx, pool.Namespace); err != nil { + return err + } + 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 +219,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 index c0ba11d4ee..823b0b29c7 100644 --- a/internal/assets/token_pool_created.go +++ b/internal/assets/token_pool_created.go @@ -25,89 +25,117 @@ import ( "github.com/hyperledger/firefly/pkg/tokens" ) -func (am *assetManager) updatePool(storedPool *fftypes.TokenPool, chainPool *tokens.TokenPool) *fftypes.TokenPool { +func updatePool(storedPool *fftypes.TokenPool, chainPool *tokens.TokenPool) *fftypes.TokenPool { storedPool.Type = chainPool.Type storedPool.ProtocolID = chainPool.ProtocolID storedPool.Key = chainPool.Key storedPool.Connector = chainPool.Connector storedPool.Standard = chainPool.Standard - storedPool.TX = fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: chainPool.TransactionID, + if chainPool.TransactionID != nil { + storedPool.TX = fftypes.TransactionRef{ + Type: fftypes.TransactionTypeTokenPool, + ID: chainPool.TransactionID, + } } return storedPool } -func (am *assetManager) TokenPoolCreated(tk tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { - var newPool *fftypes.TokenPool - var valid bool +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, + } +} - 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.TransactionID), - 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.TransactionID) - return nil - } +func (am *assetManager) confirmPool(ctx context.Context, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { + tx := poolTransaction(pool, fftypes.OpStatusSucceeded, protocolTxID, additionalInfo) + if valid, err := am.txhelper.PersistTransaction(ctx, tx); !valid || err != nil { + return err + } + pool.State = fftypes.TokenPoolStateConfirmed + if err := am.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 am.database.InsertEvent(ctx, event) +} - newPool = am.updatePool(&fftypes.TokenPool{}, pool) - err = retrieveTokenPoolCreateInputs(ctx, operations[0], newPool) - if err != nil { - log.L(ctx).Errorf("Error retrieving pool info from transaction '%s' (%s) - ignoring: %v", pool.TransactionID, err, operations[0].Input) - return nil - } +func (am *assetManager) 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 := am.database.GetOperations(ctx, filter); err != nil { + return nil, err + } else if len(operations) > 0 { + return operations[0], nil + } + return nil, 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: newPool.TX.ID, - Status: fftypes.OpStatusPending, - Subject: fftypes.TransactionSubject{ - Namespace: newPool.Namespace, - Type: newPool.TX.Type, - Signer: newPool.Key, - Reference: newPool.ID, - }, - ProtocolID: protocolTxID, - Info: additionalInfo, - } +func (am *assetManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) (err error) { + var announcePool *fftypes.TokenPool - // Add a new operation for the announcement - op := fftypes.NewTXOperation( - tk, - newPool.Namespace, - transaction.ID, - "", - fftypes.OpTypeTokenAnnouncePool, - fftypes.OpStatusPending) + 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 { + // See if this is a confirmation of an unconfirmed pool + if existingPool, err := am.database.GetTokenPoolByProtocolID(ctx, pool.Connector, pool.ProtocolID); err != nil { + return err + } else if existingPool != nil { + if existingPool.State == fftypes.TokenPoolStateConfirmed { + return nil // already confirmed + } + return am.confirmPool(ctx, updatePool(existingPool, pool), protocolTxID, additionalInfo) + } - valid, err = am.txhelper.PersistTransaction(ctx, transaction) - if valid && err == nil { - err = am.database.UpsertOperation(ctx, op, false) + // See if this pool was submitted locally and needs to be announced + if op, err := am.findTokenPoolCreateOp(ctx, pool.TransactionID); err != nil { + return err + } else if op != nil { + announcePool = updatePool(&fftypes.TokenPool{}, pool) + if err = 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) + announcePool = nil + return nil + } + nextOp := fftypes.NewTXOperation( + ti, + op.Namespace, + op.Transaction, + "", + fftypes.OpTypeTokenAnnouncePool, + fftypes.OpStatusPending) + return am.database.UpsertOperation(ctx, nextOp, false) } - return err + + // Otherwise this event can be ignored + log.L(ctx).Debugf("Ignoring token pool transaction '%s' - pool %s is not active", pool.TransactionID, pool.ProtocolID) + return nil }) return err != nil, err }) - if !valid || err != nil { - return err + if err == nil && announcePool != nil { + // 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 + broadcast := &fftypes.TokenPoolAnnouncement{ + Pool: announcePool, + TX: poolTransaction(announcePool, fftypes.OpStatusPending, protocolTxID, additionalInfo), + } + log.L(am.ctx).Infof("Announcing token pool id=%s author=%s", announcePool.ID, pool.Key) + _, err = am.broadcast.BroadcastTokenPool(am.ctx, announcePool.Namespace, broadcast, false) } - // Announce the details of the new token pool - announce := &fftypes.TokenPoolAnnouncement{ - Pool: newPool, - ProtocolTxID: protocolTxID, - } - _, err = am.broadcast.BroadcastTokenPool(am.ctx, newPool.Namespace, announce, false) return err } diff --git a/internal/assets/token_pool_created_test.go b/internal/assets/token_pool_created_test.go index dfb5e2ea04..e2b1d88ef1 100644 --- a/internal/assets/token_pool_created_test.go +++ b/internal/assets/token_pool_created_test.go @@ -17,104 +17,208 @@ package assets import ( + "fmt" "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/hyperledger/firefly/pkg/tokens" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -func TestTokenPoolCreatedSuccess(t *testing.T) { +func TestTokenPoolCreatedIgnore(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", - }, - }, - } + operations := []*fftypes.Operation{} pool := &tokens.TokenPool{ Type: fftypes.TokenTypeFungible, ProtocolID: "123", Key: "0x0", TransactionID: txID, + Connector: "erc1155", } - mti.On("Name").Return("mock-tokens") + mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, nil, nil) mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) - mdi.On("GetTransactionByID", mock.Anything, txID).Return(nil, nil) + + info := fftypes.JSONObject{"some": "info"} + err := am.TokenPoolCreated(mti, pool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) +} + +func TestTokenPoolCreatedConfirm(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + mdi := am.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, + 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("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(storedPool, nil).Once() + mdi.On("GetTransactionByID", am.ctx, txID).Return(storedTX, 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 + return *tx.Subject.Reference == *storedTX.Subject.Reference }), false).Return(nil) - mbm.On("BroadcastTokenPool", am.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) + mdi.On("UpsertTokenPool", am.ctx, storedPool).Return(nil) + mdi.On("InsertEvent", am.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { + return e.Type == fftypes.EventTypePoolConfirmed && *e.Reference == *storedPool.ID + })).Return(nil) info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, pool, "tx1", info) + err := am.TokenPoolCreated(mti, chainPool, "tx1", info) assert.NoError(t, err) mdi.AssertExpectations(t) - mbm.AssertExpectations(t) } -func TestTokenPoolCreatedOpNotFound(t *testing.T) { +func TestTokenPoolCreatedAlreadyConfirmed(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 := &tokens.TokenPool{ - Type: fftypes.TokenTypeFungible, - ProtocolID: "123", - Key: "0x0", - TransactionID: txID, + 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, + }, } - mti.On("Name").Return("mock-tokens") - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) + mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(storedPool, nil) info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, pool, "tx1", info) + err := am.TokenPoolCreated(mti, chainPool, "tx1", info) assert.NoError(t, err) mdi.AssertExpectations(t) - mbm.AssertExpectations(t) } -func TestTokenPoolMissingID(t *testing.T) { +func TestConfirmPoolTxFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + mdi := am.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", am.ctx, txID).Return(nil, fmt.Errorf("pop")) + + info := fftypes.JSONObject{"some": "info"} + err := am.confirmPool(am.ctx, storedPool, "tx1", info) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestConfirmPoolUpsertFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + mdi := am.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", am.ctx, txID).Return(storedTX, nil) + mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return *tx.Subject.Reference == *storedTX.Subject.Reference + }), false).Return(nil) + mdi.On("UpsertTokenPool", am.ctx, storedPool).Return(fmt.Errorf("pop")) + + info := fftypes.JSONObject{"some": "info"} + err := am.confirmPool(am.ctx, storedPool, "tx1", info) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestTokenPoolCreatedAnnounce(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: fftypes.NewUUID(), + Input: fftypes.JSONObject{ + "id": poolID.String(), + "namespace": "test-ns", + "name": "my-pool", + }, }, } pool := &tokens.TokenPool{ @@ -122,34 +226,41 @@ func TestTokenPoolMissingID(t *testing.T) { ProtocolID: "123", Key: "0x0", TransactionID: txID, + Connector: "erc1155", } mti.On("Name").Return("mock-tokens") - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) + mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, nil).Times(2) + mdi.On("GetOperations", am.ctx, mock.Anything).Return(nil, nil, fmt.Errorf("pop")).Once() + mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil).Once() + 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.Pool.Namespace == "test-ns" && pool.Pool.Name == "my-pool" && *pool.Pool.ID == *poolID + }), false).Return(nil, nil) info := fftypes.JSONObject{"some": "info"} err := am.TokenPoolCreated(mti, pool, "tx1", info) assert.NoError(t, err) + mti.AssertExpectations(t) mdi.AssertExpectations(t) mbm.AssertExpectations(t) } -func TestTokenPoolCreatedMissingNamespace(t *testing.T) { +func TestTokenPoolCreatedAnnounceBadOpInputID(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(), - }, + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenCreatePool, + Input: fftypes.JSONObject{}, }, } pool := &tokens.TokenPool{ @@ -157,9 +268,10 @@ func TestTokenPoolCreatedMissingNamespace(t *testing.T) { ProtocolID: "123", Key: "0x0", TransactionID: txID, + Connector: "erc1155", } - mti.On("Name").Return("mock-tokens") + mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, nil) mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) info := fftypes.JSONObject{"some": "info"} @@ -167,25 +279,21 @@ func TestTokenPoolCreatedMissingNamespace(t *testing.T) { assert.NoError(t, err) mdi.AssertExpectations(t) - mbm.AssertExpectations(t) } -func TestTokenPoolCreatedUpsertFail(t *testing.T) { +func TestTokenPoolCreatedAnnounceBadOpInputNS(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(), + ID: fftypes.NewUUID(), + Type: fftypes.OpTypeTokenCreatePool, Input: fftypes.JSONObject{ - "id": poolID.String(), - "namespace": "test-ns", - "name": "my-pool", + "id": fftypes.NewUUID().String(), }, }, } @@ -194,19 +302,15 @@ func TestTokenPoolCreatedUpsertFail(t *testing.T) { ProtocolID: "123", Key: "0x0", TransactionID: txID, + Connector: "erc1155", } - mti.On("Name").Return("mock-tokens") + mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, nil) 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..8f8fc32a5f 100644 --- a/internal/assets/token_pool_test.go +++ b/internal/assets/token_pool_test.go @@ -425,6 +425,59 @@ 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 TestActivateTokenPoolBadNamespace(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) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(fmt.Errorf("pop")) + + err := am.ActivateTokenPool(context.Background(), pool, tx) + assert.EqualError(t, err, "pop") +} + +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 +635,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/broadcast/tokenpool.go b/internal/broadcast/tokenpool.go index 2606dee647..355bd4bb54 100644 --- a/internal/broadcast/tokenpool.go +++ b/internal/broadcast/tokenpool.go @@ -23,7 +23,7 @@ import ( ) func (bm *broadcastManager) BroadcastTokenPool(ctx context.Context, ns string, pool *fftypes.TokenPoolAnnouncement, waitConfirm bool) (msg *fftypes.Message, err error) { - if err := pool.Pool.Validate(ctx, false); err != nil { + if err := pool.Pool.Validate(ctx); err != nil { return nil, err } if err := bm.data.VerifyNamespaceExists(ctx, pool.Pool.Namespace); err != nil { diff --git a/internal/broadcast/tokenpool_test.go b/internal/broadcast/tokenpool_test.go index 5cb3853be3..b1f45679ce 100644 --- a/internal/broadcast/tokenpool_test.go +++ b/internal/broadcast/tokenpool_test.go @@ -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")) @@ -69,7 +68,6 @@ func TestBroadcastTokenPoolInvalid(t *testing.T) { ProtocolID: "N1", Symbol: "COIN", }, - ProtocolTxID: "tx123", } _, err := bm.BroadcastTokenPool(context.Background(), "ns1", pool, false) @@ -95,7 +93,6 @@ func TestBroadcastTokenPoolBroadcastFail(t *testing.T) { ProtocolID: "N1", Symbol: "COIN", }, - ProtocolTxID: "tx123", } mim.On("ResolveInputIdentity", mock.Anything, mock.Anything).Return(nil) @@ -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/events/tokens_transferred.go b/internal/events/tokens_transferred.go index acb651b3e4..9b88659e7b 100644 --- a/internal/events/tokens_transferred.go +++ b/internal/events/tokens_transferred.go @@ -87,7 +87,7 @@ func (em *eventManager) TokensTransferred(tk tokens.Plugin, poolProtocolID strin 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 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 } diff --git a/internal/events/tokens_transferred_test.go b/internal/events/tokens_transferred_test.go index 597222a608..fdbf320c50 100644 --- a/internal/events/tokens_transferred_test.go +++ b/internal/events/tokens_transferred_test.go @@ -48,8 +48,8 @@ func TestTokensTransferredSucceedWithRetries(t *testing.T) { 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("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() @@ -100,7 +100,7 @@ func TestTokensTransferredWithTransactionRetries(t *testing.T) { }, }} - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(pool, 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() @@ -135,7 +135,7 @@ func TestTokensTransferredAddBalanceIgnore(t *testing.T) { Amount: *fftypes.NewBigInt(1), } - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").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) @@ -155,6 +155,7 @@ func TestTokensTransferredWithMessageReceived(t *testing.T) { transfer := &fftypes.TokenTransfer{ Type: fftypes.TokenTransferTypeTransfer, TokenIndex: "0", + Connector: "erc1155", Key: "0x12345", From: "0x1", To: "0x2", @@ -168,7 +169,7 @@ func TestTokensTransferredWithMessageReceived(t *testing.T) { BatchID: fftypes.NewUUID(), }} - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(pool, 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() @@ -210,7 +211,7 @@ func TestTokensTransferredWithMessageSend(t *testing.T) { State: fftypes.MessageStateStaged, }} - mdi.On("GetTokenPoolByProtocolID", em.ctx, "F1").Return(pool, 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) diff --git a/internal/syshandlers/syshandler_tokenpool.go b/internal/syshandlers/syshandler_tokenpool.go index a35875c3f4..2157806c62 100644 --- a/internal/syshandlers/syshandler_tokenpool.go +++ b/internal/syshandlers/syshandler_tokenpool.go @@ -24,70 +24,45 @@ import ( "github.com/hyperledger/firefly/pkg/fftypes" ) -func (sh *systemHandlers) persistTokenPool(ctx context.Context, announce *fftypes.TokenPoolAnnouncement) (valid bool, err error) { - pool := announce.Pool - +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 != announce.ProtocolTxID { - log.L(ctx).Warnf("Ignoring token pool from transaction '%s' - unexpected protocol ID '%s'", pool.TX.ID, announce.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, announce.ProtocolTxID) - err = sh.assets.ValidateTokenPoolTx(ctx, pool, announce.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: announce.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 } + // Verify pool has not already been created + if existingPool, err := sh.database.GetTokenPoolByID(ctx, pool.ID); err != nil { + return false, err // retryable + } else if existingPool != nil { + log.L(ctx).Warnf("Token pool '%s' already exists - ignoring", pool.ID) + return false, nil // not retryable + } + + // Create the pool in unconfirmed state + pool.State = fftypes.TokenPoolStatePending err = sh.database.UpsertTokenPool(ctx, pool) if err != nil { if err == database.IDMismatch { @@ -97,36 +72,40 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, announce *fftype 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 announce fftypes.TokenPoolAnnouncement if valid = sh.getSystemBroadcastPayload(ctx, msg, data, &announce); !valid { - l.Errorf("Unable to process token pool broadcast %s - message malformed: %s", msg.Header.ID, err) - return false, nil + return false, nil // not retryable } + pool := announce.Pool - if err = pool.Validate(ctx, true); err != nil { - l.Warnf("Unable to process token pool broadcast %s - validate failed: %s", msg.Header.ID, err) + 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) + err = nil // not retryable valid = false } else { - announce.Pool.Message = msg.Header.ID - if valid, err = sh.persistTokenPool(ctx, &announce); err != nil { - return valid, err + valid, err = sh.persistTokenPool(ctx, &announce) // only returns retryable errors + if err != nil { + log.L(ctx).Warnf("Token pool '%s' rejected - failed to write: %s", pool.ID, err) } } - 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) + if err != nil { + return false, err + } else if !valid { + event := fftypes.NewEvent(fftypes.EventTypePoolRejected, pool.Namespace, pool.ID) + err = sh.database.InsertEvent(ctx, event) + return err != nil, err } - err = sh.database.InsertEvent(ctx, event) - return valid, err + + 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 false, err // retryable + } + return true, nil } diff --git a/internal/syshandlers/syshandler_tokenpool_test.go b/internal/syshandlers/syshandler_tokenpool_test.go index e8e2f5d43d..8703216a0a 100644 --- a/internal/syshandlers/syshandler_tokenpool_test.go +++ b/internal/syshandlers/syshandler_tokenpool_test.go @@ -30,9 +30,7 @@ import ( "github.com/stretchr/testify/mock" ) -func TestHandleSystemBroadcastTokenPoolSelfOk(t *testing.T) { - sh := newTestSystemHandlers(t) - +func newPoolAnnouncement() *fftypes.TokenPoolAnnouncement { pool := &fftypes.TokenPool{ ID: fftypes.NewUUID(), Namespace: "ns1", @@ -45,74 +43,66 @@ func TestHandleSystemBroadcastTokenPoolSelfOk(t *testing.T) { ID: fftypes.NewUUID(), }, } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - 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(&announce) - 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 TestHandleSystemBroadcastTokenPoolOk(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) 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) - announce := &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", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&announce) + announce := newPoolAnnouncement() + msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - data := []*fftypes.Data{{ - Value: fftypes.Byteable(b), - }} opID := fftypes.NewUUID() operations := []*fftypes.Operation{{ID: opID}} @@ -127,43 +117,20 @@ func TestHandleSystemBroadcastTokenPoolSelfUpdateOpFail(t *testing.T) { mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolSelfGetTXFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolGetPoolFail(t *testing.T) { sh := newTestSystemHandlers(t) - 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(), - }, - } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&announce) + 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) @@ -172,44 +139,20 @@ func TestHandleSystemBroadcastTokenPoolSelfGetTXFail(t *testing.T) { mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolSelfTXMismatch(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolExisting(t *testing.T) { sh := newTestSystemHandlers(t) - 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(), - }, - } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&announce) + 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"} 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("GetTokenPoolByID", context.Background(), pool.ID).Return(&fftypes.TokenPool{}, 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) @@ -221,390 +164,140 @@ func TestHandleSystemBroadcastTokenPoolSelfTXMismatch(t *testing.T) { mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolSelfUpdateTXFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolIDMismatch(t *testing.T) { sh := newTestSystemHandlers(t) - 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(), - }, - } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&announce) + 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"} 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")) - - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) - assert.False(t, valid) - assert.EqualError(t, err, "pop") - - mdi.AssertExpectations(t) -} - -func TestHandleSystemBroadcastTokenPoolOk(t *testing.T) { - sh := newTestSystemHandlers(t) - - 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(), - }, - } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&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("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("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) + assert.False(t, valid) 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.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&announce) + 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) - - 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")) + 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")) valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) assert.False(t, valid) 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.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: nil, - }, - } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&announce) + announce := newPoolAnnouncement() + 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")) valid, err := sh.HandleSystemBroadcast(context.Background(), msg, data) assert.False(t, valid) - assert.NoError(t, err) + 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.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&announce) + 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.TokenPool{ - ID: fftypes.NewUUID(), - Namespace: "ns1", - Name: "name1", - Type: fftypes.TokenTypeFungible, - ProtocolID: "12345", - Symbol: "COIN", - TX: fftypes.TransactionRef{ - Type: fftypes.TransactionTypeTokenPool, - ID: fftypes.NewUUID(), - }, - } - announce := &fftypes.TokenPoolAnnouncement{ - Pool: pool, - ProtocolTxID: "tx123", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, - } - b, err := json.Marshal(&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("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) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) - mam.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolOpsFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolValidateFail(t *testing.T) { sh := newTestSystemHandlers(t) announce := &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", - } - msg := &fftypes.Message{ - Header: fftypes.MessageHeader{ - ID: fftypes.NewUUID(), - Tag: string(fftypes.SystemTagDefinePool), - }, + Pool: &fftypes.TokenPool{}, + TX: &fftypes.Transaction{}, } - b, err := json.Marshal(&announce) + 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") + assert.NoError(t, err) mdi.AssertExpectations(t) } -func TestHandleSystemBroadcastTokenPoolValidateFail(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolBadMessage(t *testing.T) { sh := newTestSystemHandlers(t) - announce := &fftypes.TokenPoolAnnouncement{ - Pool: &fftypes.TokenPool{}, - ProtocolTxID: "tx123", - } msg := &fftypes.Message{ Header: fftypes.MessageHeader{ ID: fftypes.NewUUID(), Tag: string(fftypes.SystemTagDefinePool), }, } - b, err := json.Marshal(&announce) - 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) + valid, err := sh.HandleSystemBroadcast(context.Background(), msg, nil) assert.False(t, valid) assert.NoError(t, err) - - mdi.AssertExpectations(t) } diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go index 7a3530356b..7a3d6007d7 100644 --- a/internal/tokens/fftokens/fftokens.go +++ b/internal/tokens/fftokens/fftokens.go @@ -69,6 +69,12 @@ type createPool struct { 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"` @@ -337,6 +343,20 @@ func (ft *FFTokens) CreateTokenPool(ctx context.Context, operationID *fftypes.UU 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/activatepool") + if err != nil || !res.IsSuccess() { + return restclient.WrapRestErr(ctx, res, err, i18n.MsgTokensRESTErr) + } + return nil +} + func (ft *FFTokens) MintTokens(ctx context.Context, operationID *fftypes.UUID, poolProtocolID string, mint *fftypes.TokenTransfer) error { data, _ := json.Marshal(tokenData{ TX: mint.TX.ID, diff --git a/internal/tokens/fftokens/fftokens_test.go b/internal/tokens/fftokens/fftokens_test.go index 631b79e382..4618c0e57e 100644 --- a/internal/tokens/fftokens/fftokens_test.go +++ b/internal/tokens/fftokens/fftokens_test.go @@ -163,6 +163,66 @@ func TestCreateTokenPoolError(t *testing.T) { 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() diff --git a/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index a8d9b97c24..714928b55e 100644 --- a/mocks/assetmocks/manager.go +++ b/mocks/assetmocks/manager.go @@ -20,6 +20,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) @@ -596,20 +610,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..a65a234192 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) } 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..37df2c543c 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) @@ -768,6 +768,7 @@ var TokenPoolQueryFactory = &queryFields{ "key": &StringField{}, "symbol": &StringField{}, "message": &UUIDField{}, + "state": &StringField{}, "created": &TimeField{}, "connector": &StringField{}, } diff --git a/pkg/fftypes/tokenpool.go b/pkg/fftypes/tokenpool.go index 4a4945a480..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 { - Pool *TokenPool `json:"pool"` - 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 } diff --git a/pkg/fftypes/tokenpool_test.go b/pkg/fftypes/tokenpool_test.go index 710d4de287..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) } diff --git a/pkg/tokens/plugin.go b/pkg/tokens/plugin.go index 53904b58d2..2078b52eca 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 From c8f66836687dbfbe23b2082da31370a19817a49e Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 10 Nov 2021 14:43:57 -0500 Subject: [PATCH 03/13] Ignore duplicate token transfer events Signed-off-by: Andrew Richardson --- ...000033_add_token_connector_fields.down.sql | 3 ++ .../000033_add_token_connector_fields.up.sql | 3 ++ ...000033_add_token_connector_fields.down.sql | 3 ++ .../000033_add_token_connector_fields.up.sql | 3 ++ .../database/sqlcommon/tokentransfer_sql.go | 7 ++++ .../sqlcommon/tokentransfer_sql_test.go | 7 ++++ internal/events/tokens_transferred.go | 8 ++++ internal/events/tokens_transferred_test.go | 41 ++++++++++++++++++- mocks/databasemocks/plugin.go | 23 +++++++++++ pkg/database/plugin.go | 3 ++ 10 files changed, 100 insertions(+), 1 deletion(-) 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/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/internal/database/sqlcommon/tokentransfer_sql.go b/internal/database/sqlcommon/tokentransfer_sql.go index ee8f9a1349..83663fb73a 100644 --- a/internal/database/sqlcommon/tokentransfer_sql.go +++ b/internal/database/sqlcommon/tokentransfer_sql.go @@ -185,6 +185,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..3525e83e93 100644 --- a/internal/database/sqlcommon/tokentransfer_sql_test.go +++ b/internal/database/sqlcommon/tokentransfer_sql_test.go @@ -71,6 +71,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/tokens_transferred.go b/internal/events/tokens_transferred.go index 9b88659e7b..906f15b19e 100644 --- a/internal/events/tokens_transferred.go +++ b/internal/events/tokens_transferred.go @@ -86,6 +86,14 @@ func (em *eventManager) TokensTransferred(tk tokens.Plugin, poolProtocolID strin 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, transfer.Connector, poolProtocolID) if err != nil { diff --git a/internal/events/tokens_transferred_test.go b/internal/events/tokens_transferred_test.go index fdbf320c50..1c674c745f 100644 --- a/internal/events/tokens_transferred_test.go +++ b/internal/events/tokens_transferred_test.go @@ -42,12 +42,15 @@ func TestTokensTransferredSucceedWithRetries(t *testing.T) { Key: "0x12345", From: "0x1", To: "0x2", + ProtocolID: "123", Amount: *fftypes.NewBigInt(1), } pool := &fftypes.TokenPool{ Namespace: "ns1", } + 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() @@ -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,6 +132,7 @@ func TestTokensTransferredWithTransactionRetries(t *testing.T) { }, }} + 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() @@ -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,9 +165,11 @@ func TestTokensTransferredAddBalanceIgnore(t *testing.T) { Key: "0x12345", From: "0x1", To: "0x2", + ProtocolID: "123", Amount: *fftypes.NewBigInt(1), } + 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"} @@ -159,6 +194,7 @@ func TestTokensTransferredWithMessageReceived(t *testing.T) { Key: "0x12345", From: "0x1", To: "0x2", + ProtocolID: "123", MessageHash: fftypes.NewRandB32(), Amount: *fftypes.NewBigInt(1), } @@ -169,6 +205,7 @@ func TestTokensTransferredWithMessageReceived(t *testing.T) { BatchID: fftypes.NewUUID(), }} + 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) @@ -200,6 +237,7 @@ func TestTokensTransferredWithMessageSend(t *testing.T) { Key: "0x12345", From: "0x1", To: "0x2", + ProtocolID: "123", MessageHash: fftypes.NewRandB32(), Amount: *fftypes.NewBigInt(1), } @@ -211,6 +249,7 @@ func TestTokensTransferredWithMessageSend(t *testing.T) { State: fftypes.MessageStateStaged, }} + 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) diff --git a/mocks/databasemocks/plugin.go b/mocks/databasemocks/plugin.go index a65a234192..6c4bc9bc2f 100644 --- a/mocks/databasemocks/plugin.go +++ b/mocks/databasemocks/plugin.go @@ -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/pkg/database/plugin.go b/pkg/database/plugin.go index 37df2c543c..82add48938 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -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) } From 8811020ac918abd1afd6578e29fe285ea1cff9bb Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 10 Nov 2021 15:52:50 -0500 Subject: [PATCH 04/13] Add a migration path for pools created prior to "activate" logic Pools in "unknown" state will be activated and moved to "confirmed". Signed-off-by: Andrew Richardson --- internal/assets/token_pool.go | 3 -- internal/assets/token_pool_created.go | 25 ++++++++-- internal/assets/token_pool_created_test.go | 53 ++++++++++++++++++++++ internal/assets/token_pool_test.go | 17 ------- 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/internal/assets/token_pool.go b/internal/assets/token_pool.go index c52614bde1..3a20e5f8b9 100644 --- a/internal/assets/token_pool.go +++ b/internal/assets/token_pool.go @@ -149,9 +149,6 @@ func (am *assetManager) createTokenPoolInternal(ctx context.Context, pool *fftyp } func (am *assetManager) ActivateTokenPool(ctx context.Context, pool *fftypes.TokenPool, tx *fftypes.Transaction) error { - if err := am.data.VerifyNamespaceExists(ctx, pool.Namespace); err != nil { - return err - } plugin, err := am.selectTokenPlugin(ctx, pool.Connector) if err != nil { return err diff --git a/internal/assets/token_pool_created.go b/internal/assets/token_pool_created.go index 823b0b29c7..9b73c8cba7 100644 --- a/internal/assets/token_pool_created.go +++ b/internal/assets/token_pool_created.go @@ -93,10 +93,29 @@ func (am *assetManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo if existingPool, err := am.database.GetTokenPoolByProtocolID(ctx, pool.Connector, pool.ProtocolID); err != nil { return err } else if existingPool != nil { - if existingPool.State == fftypes.TokenPoolStateConfirmed { - return nil // already confirmed + updatePool(existingPool, pool) + + switch existingPool.State { + case fftypes.TokenPoolStateConfirmed: + // Already confirmed + return nil + + case fftypes.TokenPoolStateUnknown: + // Unknown pool state - should only happen on first run after database migration + // Activate the pool, then fall through to immediately confirm + tx, err := am.database.GetTransactionByID(ctx, existingPool.TX.ID) + if err != nil { + return err + } + if err = am.ActivateTokenPool(ctx, existingPool, tx); err != nil { + log.L(ctx).Errorf("Failed to activate token pool '%s': %s", existingPool.ID, err) + return err + } + fallthrough + + default: + return am.confirmPool(ctx, existingPool, protocolTxID, additionalInfo) } - return am.confirmPool(ctx, updatePool(existingPool, pool), protocolTxID, additionalInfo) } // See if this pool was submitted locally and needs to be announced diff --git a/internal/assets/token_pool_created_test.go b/internal/assets/token_pool_created_test.go index e2b1d88ef1..ce96045456 100644 --- a/internal/assets/token_pool_created_test.go +++ b/internal/assets/token_pool_created_test.go @@ -138,6 +138,59 @@ func TestTokenPoolCreatedAlreadyConfirmed(t *testing.T) { mdi.AssertExpectations(t) } +func TestTokenPoolCreatedMigrate(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*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, + }, + } + + mdi.On("GetTokenPoolByProtocolID", am.ctx, "magic-tokens", "123").Return(storedPool, nil).Times(3) + mdi.On("GetTransactionByID", am.ctx, storedPool.TX.ID).Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetTransactionByID", am.ctx, storedPool.TX.ID).Return(storedTX, nil).Times(3) + mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return *tx.Subject.Reference == *storedTX.Subject.Reference + }), false).Return(nil).Once() + mdi.On("UpsertTokenPool", am.ctx, storedPool).Return(nil).Once() + mdi.On("InsertEvent", am.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { + return e.Type == fftypes.EventTypePoolConfirmed && *e.Reference == *storedPool.ID + })).Return(nil).Once() + mti.On("ActivateTokenPool", am.ctx, mock.Anything, storedPool, storedTX).Return(fmt.Errorf("pop")).Once() + mti.On("ActivateTokenPool", am.ctx, mock.Anything, storedPool, storedTX).Return(nil).Once() + + info := fftypes.JSONObject{"some": "info"} + err := am.TokenPoolCreated(mti, chainPool, "tx1", info) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mti.AssertExpectations(t) +} + func TestConfirmPoolTxFail(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() diff --git a/internal/assets/token_pool_test.go b/internal/assets/token_pool_test.go index 8f8fc32a5f..cf4d029188 100644 --- a/internal/assets/token_pool_test.go +++ b/internal/assets/token_pool_test.go @@ -444,23 +444,6 @@ func TestActivateTokenPool(t *testing.T) { assert.NoError(t, err) } -func TestActivateTokenPoolBadNamespace(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) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(fmt.Errorf("pop")) - - err := am.ActivateTokenPool(context.Background(), pool, tx) - assert.EqualError(t, err, "pop") -} - func TestActivateTokenPoolBadConnector(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() From 7339ee28dd7f91a3464faf0a8280ecb9150aecd6 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 10 Nov 2021 16:00:35 -0500 Subject: [PATCH 05/13] Disallow initiating token transfers in unconfirmed pools Signed-off-by: Andrew Richardson --- internal/assets/token_transfer.go | 3 ++ internal/assets/token_transfer_test.go | 47 ++++++++++++++++++++++++++ internal/i18n/en_translations.go | 1 + 3 files changed, 51 insertions(+) diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go index 06df03d436..f8462eb54e 100644 --- a/internal/assets/token_transfer.go +++ b/internal/assets/token_transfer.go @@ -274,6 +274,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 d9f00ac301..5bd13603a0 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) @@ -839,6 +881,7 @@ func TestTransferTokensWithBroadcastMessage(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -933,6 +976,7 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -1012,6 +1056,7 @@ func TestTransferTokensConfirm(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -1067,6 +1112,7 @@ func TestTransferTokensWithBroadcastConfirm(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) @@ -1125,6 +1171,7 @@ func TestTransferTokensByTypeSuccess(t *testing.T) { } pool := &fftypes.TokenPool{ ProtocolID: "F1", + State: fftypes.TokenPoolStateConfirmed, } mdi := am.database.(*databasemocks.Plugin) 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") ) From 6ca035921a19381b655091710378ab79e5ef1ded Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 10 Nov 2021 20:32:13 -0500 Subject: [PATCH 06/13] Record message ID along with message hash for token transfers Signed-off-by: Andrew Richardson --- ...00045_add_tokentransfer_messageid.down.sql | 3 ++ .../000045_add_tokentransfer_messageid.up.sql | 9 ++++ ...00045_add_tokentransfer_messageid.down.sql | 1 + .../000045_add_tokentransfer_messageid.up.sql | 5 ++ docs/swagger/swagger.yaml | 25 ++++++++++ internal/assets/token_transfer.go | 3 +- internal/assets/token_transfer_test.go | 19 +++++-- .../database/sqlcommon/tokentransfer_sql.go | 5 ++ .../sqlcommon/tokentransfer_sql_test.go | 1 + internal/events/aggregator.go | 7 ++- internal/events/aggregator_test.go | 31 ++++++++++++ internal/events/tokens_transferred.go | 28 +++-------- internal/events/tokens_transferred_test.go | 50 +++++++++---------- internal/tokens/fftokens/fftokens.go | 5 ++ internal/tokens/fftokens/fftokens_test.go | 17 ++++--- pkg/database/plugin.go | 1 + pkg/fftypes/tokentransfer.go | 1 + 17 files changed, 150 insertions(+), 61 deletions(-) create mode 100644 db/migrations/postgres/000045_add_tokentransfer_messageid.down.sql create mode 100644 db/migrations/postgres/000045_add_tokentransfer_messageid.up.sql create mode 100644 db/migrations/sqlite/000045_add_tokentransfer_messageid.down.sql create mode 100644 db/migrations/sqlite/000045_add_tokentransfer_messageid.up.sql 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/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 947505203d..906f94e3dd 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -4772,6 +4772,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -4806,6 +4807,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -4989,6 +4991,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5023,6 +5026,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5107,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 @@ -5187,6 +5196,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5370,6 +5380,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5404,6 +5415,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5894,6 +5906,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -5928,6 +5941,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6131,6 +6145,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6165,6 +6180,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6583,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 @@ -6663,6 +6684,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6833,6 +6855,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6867,6 +6890,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string @@ -6929,6 +6953,7 @@ paths: key: type: string localId: {} + message: {} messageHash: {} namespace: type: string diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go index f8462eb54e..b4a37d584a 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 } diff --git a/internal/assets/token_transfer_test.go b/internal/assets/token_transfer_test.go index 5bd13603a0..c8e1ca85f5 100644 --- a/internal/assets/token_transfer_test.go +++ b/internal/assets/token_transfer_test.go @@ -860,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{ @@ -870,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{ @@ -904,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) @@ -952,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{ @@ -963,6 +969,7 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) { Message: &fftypes.MessageInOut{ Message: fftypes.Message{ Header: fftypes.MessageHeader{ + ID: msgID, Type: fftypes.MessageTypeTransferPrivate, }, Hash: hash, @@ -999,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) @@ -1091,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{ @@ -1101,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{ @@ -1148,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) diff --git a/internal/database/sqlcommon/tokentransfer_sql.go b/internal/database/sqlcommon/tokentransfer_sql.go index 83663fb73a..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, diff --git a/internal/database/sqlcommon/tokentransfer_sql_test.go b/internal/database/sqlcommon/tokentransfer_sql_test.go index 3525e83e93..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, diff --git a/internal/events/aggregator.go b/internal/events/aggregator.go index 12da8eb529..00ed8d32ff 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 } } diff --git a/internal/events/aggregator_test.go b/internal/events/aggregator_test.go index dec44a69f9..b6e7593b41 100644 --- a/internal/events/aggregator_test.go +++ b/internal/events/aggregator_test.go @@ -932,6 +932,37 @@ 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() diff --git a/internal/events/tokens_transferred.go b/internal/events/tokens_transferred.go index 906f15b19e..08d5e7b7f9 100644 --- a/internal/events/tokens_transferred.go +++ b/internal/events/tokens_transferred.go @@ -67,20 +67,6 @@ 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 { var batchID *fftypes.UUID @@ -127,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 } @@ -152,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 1c674c745f..c5844a5f12 100644 --- a/internal/events/tokens_transferred_test.go +++ b/internal/events/tokens_transferred_test.go @@ -188,29 +188,29 @@ func TestTokensTransferredWithMessageReceived(t *testing.T) { mti := &tokenmocks.Plugin{} transfer := &fftypes.TokenTransfer{ - Type: fftypes.TokenTransferTypeTransfer, - TokenIndex: "0", - Connector: "erc1155", - Key: "0x12345", - From: "0x1", - To: "0x2", - ProtocolID: "123", - 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("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() @@ -231,29 +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", - ProtocolID: "123", - 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("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/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go index 7a3d6007d7..f8abab72fb 100644 --- a/internal/tokens/fftokens/fftokens.go +++ b/internal/tokens/fftokens/fftokens.go @@ -58,6 +58,7 @@ const ( type tokenData struct { TX *fftypes.UUID `json:"tx,omitempty"` + Message *fftypes.UUID `json:"message,omitempty"` MessageHash *fftypes.Bytes32 `json:"messageHash,omitempty"` } @@ -251,6 +252,7 @@ func (ft *FFTokens) handleTokenTransfer(ctx context.Context, t fftypes.TokenTran To: toAddress, ProtocolID: txHash, Key: operatorAddress, + Message: transferData.Message, MessageHash: transferData.MessageHash, TX: fftypes.TransactionRef{ ID: transferData.TX, @@ -360,6 +362,7 @@ func (ft *FFTokens) ActivateTokenPool(ctx context.Context, operationID *fftypes. 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). @@ -381,6 +384,7 @@ 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). @@ -403,6 +407,7 @@ 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). diff --git a/internal/tokens/fftokens/fftokens_test.go b/internal/tokens/fftokens/fftokens_test.go index 4618c0e57e..bc36eefe49 100644 --- a/internal/tokens/fftokens/fftokens_test.go +++ b/internal/tokens/fftokens/fftokens_test.go @@ -479,7 +479,7 @@ func TestEvents(t *testing.T) { // token-pool: 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 == *txID - }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil) + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() fromServer <- fftypes.JSONObject{ "id": "8", "event": "token-pool", @@ -526,7 +526,7 @@ func TestEvents(t *testing.T) { // 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) + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() fromServer <- fftypes.JSONObject{ "id": "11", "event": "token-mint", @@ -547,7 +547,7 @@ func TestEvents(t *testing.T) { // 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) + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() fromServer <- fftypes.JSONObject{ "id": "12", "event": "token-mint", @@ -588,7 +588,7 @@ func TestEvents(t *testing.T) { // 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) + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() fromServer <- fftypes.JSONObject{ "id": "14", "event": "token-transfer", @@ -608,9 +608,10 @@ func TestEvents(t *testing.T) { 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) + 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", @@ -620,7 +621,7 @@ func TestEvents(t *testing.T) { "from": "0x0", "to": "0x1", "amount": "2", - "data": fftypes.JSONObject{"tx": txID.String()}.String(), + "data": fftypes.JSONObject{"tx": txID.String(), "message": messageID.String()}.String(), "transaction": fftypes.JSONObject{ "transactionHash": "abc", }, @@ -632,7 +633,7 @@ func TestEvents(t *testing.T) { // 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) + }), "abc", fftypes.JSONObject{"transactionHash": "abc"}).Return(nil).Once() fromServer <- fftypes.JSONObject{ "id": "16", "event": "token-burn", diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go index 82add48938..39bf051ae6 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -799,6 +799,7 @@ var TokenTransferQueryFactory = &queryFields{ "to": &StringField{}, "amount": &Int64Field{}, "protocolid": &StringField{}, + "message": &UUIDField{}, "messagehash": &Bytes32Field{}, "created": &TimeField{}, } 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"` From f2b7bf00aaefb75ca5af3858eb1dcaed63d57817 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 10 Nov 2021 21:02:12 -0500 Subject: [PATCH 07/13] Move TokenPoolCreated handler to event manager Signed-off-by: Andrew Richardson --- internal/assets/manager.go | 3 - internal/assets/token_pool.go | 30 +---- internal/events/event_manager.go | 10 +- internal/events/event_manager_test.go | 9 +- .../{assets => events}/token_pool_created.go | 39 +++--- .../token_pool_created_test.go | 124 +++++++++--------- internal/events/tokens_transferred.go | 2 +- internal/orchestrator/bound_callbacks.go | 4 +- internal/orchestrator/bound_callbacks_test.go | 6 +- internal/orchestrator/orchestrator.go | 3 +- internal/txcommon/token_inputs.go | 27 ++++ internal/txcommon/token_inputs_test.go | 71 ++++++++++ mocks/assetmocks/manager.go | 16 --- mocks/eventmocks/event_manager.go | 22 +++- 14 files changed, 219 insertions(+), 147 deletions(-) rename internal/{assets => events}/token_pool_created.go (79%) rename internal/{assets => events}/token_pool_created_test.go (69%) diff --git a/internal/assets/manager.go b/internal/assets/manager.go index 9cc49017db..8921bf6917 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -55,9 +55,6 @@ type Manager interface { GetTokenConnectors(ctx context.Context, ns string) ([]*fftypes.TokenConnector, error) - // Bound token callbacks - TokenPoolCreated(ti tokens.Plugin, pool *tokens.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 3a20e5f8b9..4e7f08048f 100644 --- a/internal/assets/token_pool.go +++ b/internal/assets/token_pool.go @@ -18,39 +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, - "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 (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 @@ -132,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 */) 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/assets/token_pool_created.go b/internal/events/token_pool_created.go similarity index 79% rename from internal/assets/token_pool_created.go rename to internal/events/token_pool_created.go index 9b73c8cba7..3314821ec2 100644 --- a/internal/assets/token_pool_created.go +++ b/internal/events/token_pool_created.go @@ -14,12 +14,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package assets +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" @@ -55,28 +56,28 @@ func poolTransaction(pool *fftypes.TokenPool, status fftypes.OpStatus, protocolT } } -func (am *assetManager) confirmPool(ctx context.Context, pool *fftypes.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { +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 := am.txhelper.PersistTransaction(ctx, tx); !valid || err != nil { + if valid, err := em.txhelper.PersistTransaction(ctx, tx); !valid || err != nil { return err } pool.State = fftypes.TokenPoolStateConfirmed - if err := am.database.UpsertTokenPool(ctx, pool); err != nil { + 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 am.database.InsertEvent(ctx, event) + return em.database.InsertEvent(ctx, event) } -func (am *assetManager) findTokenPoolCreateOp(ctx context.Context, tx *fftypes.UUID) (*fftypes.Operation, error) { +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 := am.database.GetOperations(ctx, filter); err != nil { + if operations, _, err := em.database.GetOperations(ctx, filter); err != nil { return nil, err } else if len(operations) > 0 { return operations[0], nil @@ -84,13 +85,13 @@ func (am *assetManager) findTokenPoolCreateOp(ctx context.Context, tx *fftypes.U return nil, nil } -func (am *assetManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) (err error) { +func (em *eventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) (err error) { var announcePool *fftypes.TokenPool - 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 { + 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 := am.database.GetTokenPoolByProtocolID(ctx, pool.Connector, pool.ProtocolID); err != nil { + if existingPool, err := em.database.GetTokenPoolByProtocolID(ctx, pool.Connector, pool.ProtocolID); err != nil { return err } else if existingPool != nil { updatePool(existingPool, pool) @@ -103,27 +104,27 @@ func (am *assetManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo case fftypes.TokenPoolStateUnknown: // Unknown pool state - should only happen on first run after database migration // Activate the pool, then fall through to immediately confirm - tx, err := am.database.GetTransactionByID(ctx, existingPool.TX.ID) + tx, err := em.database.GetTransactionByID(ctx, existingPool.TX.ID) if err != nil { return err } - if err = am.ActivateTokenPool(ctx, existingPool, tx); err != nil { + 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 err } fallthrough default: - return am.confirmPool(ctx, existingPool, protocolTxID, additionalInfo) + return em.confirmPool(ctx, existingPool, protocolTxID, additionalInfo) } } // See if this pool was submitted locally and needs to be announced - if op, err := am.findTokenPoolCreateOp(ctx, pool.TransactionID); err != nil { + if op, err := em.findTokenPoolCreateOp(ctx, pool.TransactionID); err != nil { return err } else if op != nil { announcePool = updatePool(&fftypes.TokenPool{}, pool) - if err = retrieveTokenPoolCreateInputs(ctx, op, announcePool); err != nil { + 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) announcePool = nil return nil @@ -135,7 +136,7 @@ func (am *assetManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo "", fftypes.OpTypeTokenAnnouncePool, fftypes.OpStatusPending) - return am.database.UpsertOperation(ctx, nextOp, false) + return em.database.UpsertOperation(ctx, nextOp, false) } // Otherwise this event can be ignored @@ -152,8 +153,8 @@ func (am *assetManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo Pool: announcePool, TX: poolTransaction(announcePool, fftypes.OpStatusPending, protocolTxID, additionalInfo), } - log.L(am.ctx).Infof("Announcing token pool id=%s author=%s", announcePool.ID, pool.Key) - _, err = am.broadcast.BroadcastTokenPool(am.ctx, announcePool.Namespace, broadcast, false) + 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/assets/token_pool_created_test.go b/internal/events/token_pool_created_test.go similarity index 69% rename from internal/assets/token_pool_created_test.go rename to internal/events/token_pool_created_test.go index ce96045456..e5e100a6b7 100644 --- a/internal/assets/token_pool_created_test.go +++ b/internal/events/token_pool_created_test.go @@ -14,12 +14,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package assets +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" @@ -30,9 +31,9 @@ import ( ) func TestTokenPoolCreatedIgnore(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) mti := &tokenmocks.Plugin{} txID := fftypes.NewUUID() @@ -45,20 +46,20 @@ func TestTokenPoolCreatedIgnore(t *testing.T) { Connector: "erc1155", } - mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, nil, nil) - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) + 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 := am.TokenPoolCreated(mti, pool, "tx1", info) + err := em.TokenPoolCreated(mti, pool, "tx1", info) assert.NoError(t, err) mdi.AssertExpectations(t) } func TestTokenPoolCreatedConfirm(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) mti := &tokenmocks.Plugin{} txID := fftypes.NewUUID() @@ -87,28 +88,28 @@ func TestTokenPoolCreatedConfirm(t *testing.T) { }, } - mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, fmt.Errorf("pop")).Once() - mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(storedPool, nil).Once() - mdi.On("GetTransactionByID", am.ctx, txID).Return(storedTX, nil) - mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(nil, fmt.Errorf("pop")).Once() + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(storedPool, nil).Once() + 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", am.ctx, storedPool).Return(nil) - mdi.On("InsertEvent", am.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { + 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) info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, chainPool, "tx1", info) + err := em.TokenPoolCreated(mti, chainPool, "tx1", info) assert.NoError(t, err) mdi.AssertExpectations(t) } func TestTokenPoolCreatedAlreadyConfirmed(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) mti := &tokenmocks.Plugin{} txID := fftypes.NewUUID() @@ -129,20 +130,21 @@ func TestTokenPoolCreatedAlreadyConfirmed(t *testing.T) { }, } - mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(storedPool, nil) + mdi.On("GetTokenPoolByProtocolID", em.ctx, "erc1155", "123").Return(storedPool, nil) info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, chainPool, "tx1", info) + err := em.TokenPoolCreated(mti, chainPool, "tx1", info) assert.NoError(t, err) mdi.AssertExpectations(t) } func TestTokenPoolCreatedMigrate(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) + mam := em.assets.(*assetmocks.Manager) + mti := &tokenmocks.Plugin{} txID := fftypes.NewUUID() chainPool := &tokens.TokenPool{ @@ -170,31 +172,31 @@ func TestTokenPoolCreatedMigrate(t *testing.T) { }, } - mdi.On("GetTokenPoolByProtocolID", am.ctx, "magic-tokens", "123").Return(storedPool, nil).Times(3) - mdi.On("GetTransactionByID", am.ctx, storedPool.TX.ID).Return(nil, fmt.Errorf("pop")).Once() - mdi.On("GetTransactionByID", am.ctx, storedPool.TX.ID).Return(storedTX, nil).Times(3) - mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + 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", am.ctx, storedPool).Return(nil).Once() - mdi.On("InsertEvent", am.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { + 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() - mti.On("ActivateTokenPool", am.ctx, mock.Anything, storedPool, storedTX).Return(fmt.Errorf("pop")).Once() - mti.On("ActivateTokenPool", am.ctx, mock.Anything, storedPool, storedTX).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() info := fftypes.JSONObject{"some": "info"} - err := am.TokenPoolCreated(mti, chainPool, "tx1", info) + err := em.TokenPoolCreated(mti, chainPool, "tx1", info) assert.NoError(t, err) mdi.AssertExpectations(t) - mti.AssertExpectations(t) + mam.AssertExpectations(t) } func TestConfirmPoolTxFail(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) txID := fftypes.NewUUID() storedPool := &fftypes.TokenPool{ @@ -208,19 +210,19 @@ func TestConfirmPoolTxFail(t *testing.T) { }, } - mdi.On("GetTransactionByID", am.ctx, txID).Return(nil, fmt.Errorf("pop")) + mdi.On("GetTransactionByID", em.ctx, txID).Return(nil, fmt.Errorf("pop")) info := fftypes.JSONObject{"some": "info"} - err := am.confirmPool(am.ctx, storedPool, "tx1", info) + err := em.confirmPool(em.ctx, storedPool, "tx1", info) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) } func TestConfirmPoolUpsertFail(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) txID := fftypes.NewUUID() storedPool := &fftypes.TokenPool{ @@ -242,25 +244,25 @@ func TestConfirmPoolUpsertFail(t *testing.T) { }, } - mdi.On("GetTransactionByID", am.ctx, txID).Return(storedTX, nil) - mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + 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", am.ctx, storedPool).Return(fmt.Errorf("pop")) + mdi.On("UpsertTokenPool", em.ctx, storedPool).Return(fmt.Errorf("pop")) info := fftypes.JSONObject{"some": "info"} - err := am.confirmPool(am.ctx, storedPool, "tx1", info) + err := em.confirmPool(em.ctx, storedPool, "tx1", info) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) } func TestTokenPoolCreatedAnnounce(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) mti := &tokenmocks.Plugin{} - mbm := am.broadcast.(*broadcastmocks.Manager) + mbm := em.broadcast.(*broadcastmocks.Manager) poolID := fftypes.NewUUID() txID := fftypes.NewUUID() @@ -283,18 +285,18 @@ func TestTokenPoolCreatedAnnounce(t *testing.T) { } mti.On("Name").Return("mock-tokens") - mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, nil).Times(2) - mdi.On("GetOperations", am.ctx, mock.Anything).Return(nil, nil, fmt.Errorf("pop")).Once() - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil).Once() - mdi.On("UpsertOperation", am.ctx, mock.MatchedBy(func(op *fftypes.Operation) bool { + 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", am.ctx, "test-ns", mock.MatchedBy(func(pool *fftypes.TokenPoolAnnouncement) bool { + 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 := am.TokenPoolCreated(mti, pool, "tx1", info) + err := em.TokenPoolCreated(mti, pool, "tx1", info) assert.NoError(t, err) mti.AssertExpectations(t) @@ -303,9 +305,9 @@ func TestTokenPoolCreatedAnnounce(t *testing.T) { } func TestTokenPoolCreatedAnnounceBadOpInputID(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) mti := &tokenmocks.Plugin{} txID := fftypes.NewUUID() @@ -324,20 +326,20 @@ func TestTokenPoolCreatedAnnounceBadOpInputID(t *testing.T) { Connector: "erc1155", } - mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, nil) - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) + 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 := am.TokenPoolCreated(mti, pool, "tx1", info) + err := em.TokenPoolCreated(mti, pool, "tx1", info) assert.NoError(t, err) mdi.AssertExpectations(t) } func TestTokenPoolCreatedAnnounceBadOpInputNS(t *testing.T) { - am, cancel := newTestAssets(t) + em, cancel := newTestEventManager(t) defer cancel() - mdi := am.database.(*databasemocks.Plugin) + mdi := em.database.(*databasemocks.Plugin) mti := &tokenmocks.Plugin{} txID := fftypes.NewUUID() @@ -358,11 +360,11 @@ func TestTokenPoolCreatedAnnounceBadOpInputNS(t *testing.T) { Connector: "erc1155", } - mdi.On("GetTokenPoolByProtocolID", am.ctx, "erc1155", "123").Return(nil, nil) - mdi.On("GetOperations", am.ctx, mock.Anything).Return(operations, nil, nil) + 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 := am.TokenPoolCreated(mti, pool, "tx1", 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 08d5e7b7f9..d4f1cde2d9 100644 --- a/internal/events/tokens_transferred.go +++ b/internal/events/tokens_transferred.go @@ -67,7 +67,7 @@ func (em *eventManager) persistTokenTransaction(ctx context.Context, ns string, return em.txhelper.PersistTransaction(ctx, transaction) } -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) { diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go index e802c88011..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 { @@ -57,7 +55,7 @@ func (bc *boundCallbacks) MessageReceived(peerID string, data []byte) error { } func (bc *boundCallbacks) TokenPoolCreated(plugin tokens.Plugin, pool *tokens.TokenPool, protocolTxID string, additionalInfo fftypes.JSONObject) error { - return bc.am.TokenPoolCreated(plugin, pool, protocolTxID, additionalInfo) + 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 d18ae54242..d8c9a0abf5 100644 --- a/internal/orchestrator/bound_callbacks_test.go +++ b/internal/orchestrator/bound_callbacks_test.go @@ -20,7 +20,6 @@ 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" @@ -36,8 +35,7 @@ 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()} @@ -70,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/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/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index 714928b55e..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 @@ -550,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 *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 -} - // TransferTokens provides a mock function with given fields: ctx, ns, transfer, waitConfirm func (_m *Manager) TransferTokens(ctx context.Context, ns string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (*fftypes.TokenTransfer, error) { ret := _m.Called(ctx, ns, transfer, waitConfirm) diff --git a/mocks/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) } From 476aea262e6f77a012cca15ab4c376497e0522dd Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 11 Nov 2021 10:43:54 -0500 Subject: [PATCH 08/13] Do not confirm token pool message until pool is confirmed Signed-off-by: Andrew Richardson --- internal/events/aggregator.go | 13 +-- internal/events/aggregator_test.go | 5 +- internal/events/token_pool_created.go | 27 ++++-- internal/events/token_pool_created_test.go | 12 ++- internal/syshandlers/syshandler.go | 33 +++++-- .../syshandlers/syshandler_datatype_test.go | 32 +++---- .../syshandlers/syshandler_namespace_test.go | 40 ++++----- .../syshandler_network_node_test.go | 44 +++++----- .../syshandler_network_org_test.go | 44 +++++----- internal/syshandlers/syshandler_test.go | 10 +-- internal/syshandlers/syshandler_tokenpool.go | 56 ++++++------ .../syshandlers/syshandler_tokenpool_test.go | 85 ++++++++++++------- mocks/syshandlersmocks/system_handlers.go | 10 ++- 13 files changed, 243 insertions(+), 168 deletions(-) diff --git a/internal/events/aggregator.go b/internal/events/aggregator.go index 00ed8d32ff..d7ab6bc2a6 100644 --- a/internal/events/aggregator.go +++ b/internal/events/aggregator.go @@ -434,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 b6e7593b41..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" @@ -968,7 +969,7 @@ func TestAttemptMessageDispatchFailValidateBadSystem(t *testing.T) { 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) @@ -1011,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/token_pool_created.go b/internal/events/token_pool_created.go index 3314821ec2..2fc55b9e6f 100644 --- a/internal/events/token_pool_created.go +++ b/internal/events/token_pool_created.go @@ -86,6 +86,7 @@ func (em *eventManager) findTokenPoolCreateOp(ctx context.Context, tx *fftypes.U } 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) { @@ -115,6 +116,12 @@ func (em *eventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo fallthrough default: + // Confirm the pool and identify its definition message + if msg, err := em.database.GetMessageByID(ctx, existingPool.Message); err != nil { + return err + } else if msg != nil { + batchID = msg.BatchID + } return em.confirmPool(ctx, existingPool, protocolTxID, additionalInfo) } } @@ -146,15 +153,23 @@ func (em *eventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo return err != nil, err }) - if err == nil && announcePool != nil { + 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 - broadcast := &fftypes.TokenPoolAnnouncement{ - Pool: announcePool, - TX: poolTransaction(announcePool, fftypes.OpStatusPending, protocolTxID, additionalInfo), + 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) } - 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 index e5e100a6b7..91344cbda7 100644 --- a/internal/events/token_pool_created_test.go +++ b/internal/events/token_pool_created_test.go @@ -74,6 +74,7 @@ func TestTokenPoolCreatedConfirm(t *testing.T) { ID: fftypes.NewUUID(), Key: chainPool.Key, State: fftypes.TokenPoolStatePending, + Message: fftypes.NewUUID(), TX: fftypes.TransactionRef{ Type: fftypes.TransactionTypeTokenPool, ID: txID, @@ -87,9 +88,12 @@ func TestTokenPoolCreatedConfirm(t *testing.T) { 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).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 @@ -98,6 +102,8 @@ func TestTokenPoolCreatedConfirm(t *testing.T) { mdi.On("InsertEvent", em.ctx, mock.MatchedBy(func(e *fftypes.Event) bool { return e.Type == fftypes.EventTypePoolConfirmed && *e.Reference == *storedPool.ID })).Return(nil) + 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) @@ -171,6 +177,9 @@ func TestTokenPoolCreatedMigrate(t *testing.T) { 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() @@ -184,6 +193,7 @@ func TestTokenPoolCreatedMigrate(t *testing.T) { })).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) 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 2157806c62..054c694ac5 100644 --- a/internal/syshandlers/syshandler_tokenpool.go +++ b/internal/syshandlers/syshandler_tokenpool.go @@ -53,15 +53,7 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, announce *fftype return false, err // retryable } - // Verify pool has not already been created - if existingPool, err := sh.database.GetTokenPoolByID(ctx, pool.ID); err != nil { - return false, err // retryable - } else if existingPool != nil { - log.L(ctx).Warnf("Token pool '%s' already exists - ignoring", pool.ID) - return false, nil // not retryable - } - - // Create the pool in unconfirmed state + // Create the pool in pending state pool.State = fftypes.TokenPoolStatePending err = sh.database.UpsertTokenPool(ctx, pool) if err != nil { @@ -76,36 +68,44 @@ func (sh *systemHandlers) persistTokenPool(ctx context.Context, announce *fftype return true, nil } -func (sh *systemHandlers) handleTokenPoolBroadcast(ctx context.Context, msg *fftypes.Message, data []*fftypes.Data) (valid bool, err error) { +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 false, nil // not retryable + if valid := sh.getSystemBroadcastPayload(ctx, msg, data, &announce); !valid { + return ActionReject, nil } pool := announce.Pool pool.Message = msg.Header.ID - if err = pool.Validate(ctx); err != nil { + + if err := pool.Validate(ctx); err != nil { log.L(ctx).Warnf("Token pool '%s' rejected - validate failed: %s", pool.ID, err) - err = nil // not retryable - valid = false - } else { - valid, err = sh.persistTokenPool(ctx, &announce) // only returns retryable errors - if err != nil { - log.L(ctx).Warnf("Token pool '%s' rejected - failed to write: %s", pool.ID, err) - } + return ActionReject, sh.rejectPool(ctx, pool) } - if err != nil { - return false, 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 { - event := fftypes.NewEvent(fftypes.EventTypePoolRejected, pool.Namespace, pool.ID) - err = sh.database.InsertEvent(ctx, event) - return err != nil, err + return ActionReject, sh.rejectPool(ctx, pool) } - if err = sh.assets.ActivateTokenPool(ctx, pool, announce.TX); err != nil { + 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 false, err // retryable + return ActionRetry, err } - return true, nil + + // 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 8703216a0a..01852c0b07 100644 --- a/internal/syshandlers/syshandler_tokenpool_test.go +++ b/internal/syshandlers/syshandler_tokenpool_test.go @@ -66,7 +66,7 @@ func buildPoolDefinitionMessage(announce *fftypes.TokenPoolAnnouncement) (*fftyp return msg, data, nil } -func TestHandleSystemBroadcastTokenPoolOk(t *testing.T) { +func TestHandleSystemBroadcastTokenPoolActivateOK(t *testing.T) { sh := newTestSystemHandlers(t) announce := newPoolAnnouncement() @@ -90,8 +90,8 @@ func TestHandleSystemBroadcastTokenPoolOk(t *testing.T) { 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) mdi.AssertExpectations(t) @@ -101,6 +101,7 @@ func TestHandleSystemBroadcastTokenPoolUpdateOpFail(t *testing.T) { sh := newTestSystemHandlers(t) announce := newPoolAnnouncement() + pool := announce.Pool msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) opID := fftypes.NewUUID() @@ -109,9 +110,10 @@ func TestHandleSystemBroadcastTokenPoolUpdateOpFail(t *testing.T) { 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) @@ -124,16 +126,12 @@ func TestHandleSystemBroadcastTokenPoolGetPoolFail(t *testing.T) { pool := announce.Pool msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - 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, 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) @@ -146,19 +144,44 @@ func TestHandleSystemBroadcastTokenPoolExisting(t *testing.T) { pool := announce.Pool msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) - opID := fftypes.NewUUID() - operations := []*fftypes.Operation{{ID: opID}} + 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("GetTokenPoolByID", context.Background(), pool.ID).Return(&fftypes.TokenPool{}, 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("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 TestHandleSystemBroadcastTokenPoolExistingConfirmed(t *testing.T) { + sh := newTestSystemHandlers(t) + + announce := newPoolAnnouncement() + pool := announce.Pool + msg, data, err := buildPoolDefinitionMessage(announce) + assert.NoError(t, err) + existing := &fftypes.TokenPool{ + State: fftypes.TokenPoolStateConfirmed, + } + + mdi := sh.database.(*databasemocks.Plugin) + mdi.On("GetTokenPoolByID", context.Background(), pool.ID).Return(existing, nil) + + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionConfirm, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -185,8 +208,8 @@ func TestHandleSystemBroadcastTokenPoolIDMismatch(t *testing.T) { return *event.Reference == *pool.ID && event.Namespace == pool.Namespace && 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, data) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -210,8 +233,8 @@ func TestHandleSystemBroadcastTokenPoolFailUpsert(t *testing.T) { return *p.ID == *pool.ID && p.Message == msg.Header.ID })).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) @@ -221,14 +244,16 @@ func TestHandleSystemBroadcastTokenPoolOpsFail(t *testing.T) { sh := newTestSystemHandlers(t) announce := newPoolAnnouncement() + pool := announce.Pool msg, data, err := buildPoolDefinitionMessage(announce) assert.NoError(t, err) mdi := sh.database.(*databasemocks.Plugin) 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) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, data) + assert.Equal(t, ActionRetry, action) assert.EqualError(t, err, "pop") mdi.AssertExpectations(t) @@ -258,8 +283,8 @@ func TestHandleSystemBroadcastTokenPoolActivateFail(t *testing.T) { return true })).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) @@ -280,8 +305,8 @@ func TestHandleSystemBroadcastTokenPoolValidateFail(t *testing.T) { 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, data) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) mdi.AssertExpectations(t) @@ -297,7 +322,7 @@ func TestHandleSystemBroadcastTokenPoolBadMessage(t *testing.T) { }, } - valid, err := sh.HandleSystemBroadcast(context.Background(), msg, nil) - assert.False(t, valid) + action, err := sh.HandleSystemBroadcast(context.Background(), msg, nil) + assert.Equal(t, ActionReject, action) assert.NoError(t, err) } 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 From 4cab02991deec6a9dcfee48e1ce74fe08e095931 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 11 Nov 2021 11:14:02 -0500 Subject: [PATCH 09/13] Break up some logic in token pool created handler Signed-off-by: Andrew Richardson --- internal/events/token_pool_created.go | 106 +++++++++++++++----------- 1 file changed, 61 insertions(+), 45 deletions(-) diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go index 2fc55b9e6f..a268508bd7 100644 --- a/internal/events/token_pool_created.go +++ b/internal/events/token_pool_created.go @@ -85,6 +85,55 @@ func (em *eventManager) findTokenPoolCreateOp(ctx context.Context, tx *fftypes.U 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 + } + updatePool(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) { + op, err := em.findTokenPoolCreateOp(ctx, pool.TransactionID) + if err != nil { + return nil, err + } else if op == nil { + return nil, nil + } + announcePool = updatePool(&fftypes.TokenPool{}, pool) + + 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 + } + 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 @@ -92,58 +141,25 @@ func (em *eventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo 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.database.GetTokenPoolByProtocolID(ctx, pool.Connector, pool.ProtocolID); err != nil { + if existingPool, err := em.shouldConfirm(ctx, pool); err != nil { return err } else if existingPool != nil { - updatePool(existingPool, pool) - - switch existingPool.State { - case fftypes.TokenPoolStateConfirmed: - // Already confirmed - return nil - - case fftypes.TokenPoolStateUnknown: - // Unknown pool state - should only happen on first run after database migration - // Activate the pool, then fall through to immediately confirm - tx, err := em.database.GetTransactionByID(ctx, existingPool.TX.ID) - if err != nil { - return 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 err - } - fallthrough - - default: - // Confirm the pool and identify its definition message - if msg, err := em.database.GetMessageByID(ctx, existingPool.Message); err != nil { - return err - } else if msg != nil { - batchID = msg.BatchID - } - return em.confirmPool(ctx, existingPool, protocolTxID, additionalInfo) + 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 op, err := em.findTokenPoolCreateOp(ctx, pool.TransactionID); err != nil { + if announcePool, err = em.shouldAnnounce(ctx, ti, pool); err != nil { return err - } else if op != nil { - announcePool = updatePool(&fftypes.TokenPool{}, pool) - 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) - announcePool = nil - return nil - } - nextOp := fftypes.NewTXOperation( - ti, - op.Namespace, - op.Transaction, - "", - fftypes.OpTypeTokenAnnouncePool, - fftypes.OpStatusPending) - return em.database.UpsertOperation(ctx, nextOp, false) + } else if announcePool != nil { + return nil // trigger announce after completion of database transaction } // Otherwise this event can be ignored From 31540055a5efdb217b4ef90b2f72e3f019474c0d Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 11 Nov 2021 11:35:17 -0500 Subject: [PATCH 10/13] Allow pool events to be missing a transaction ID A transaction ID is still required to trigger announcement, so the initial event back to the submitter needs to reflect the data passed into the connector. However, the later events triggered to all nodes technically do not need the transaction ID - so do not enforce this as a hard requirement. Signed-off-by: Andrew Richardson --- internal/events/token_pool_created.go | 6 +++++- internal/events/token_pool_created_test.go | 23 ++++++++++++++++++++++ internal/tokens/fftokens/fftokens.go | 12 ++++++----- internal/tokens/fftokens/fftokens_test.go | 7 +++++-- pkg/tokens/plugin.go | 22 ++++++++++++++++----- 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go index a268508bd7..d4ff8f0680 100644 --- a/internal/events/token_pool_created.go +++ b/internal/events/token_pool_created.go @@ -108,6 +108,10 @@ func (em *eventManager) shouldConfirm(ctx context.Context, pool *tokens.TokenPoo } 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 @@ -163,7 +167,7 @@ func (em *eventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo } // Otherwise this event can be ignored - log.L(ctx).Debugf("Ignoring token pool transaction '%s' - pool %s is not active", pool.TransactionID, pool.ProtocolID) + log.L(ctx).Debugf("Ignoring token pool transaction '%s' - pool %s is not active", protocolTxID, pool.ProtocolID) return nil }) return err != nil, err diff --git a/internal/events/token_pool_created_test.go b/internal/events/token_pool_created_test.go index 91344cbda7..f1eef19b13 100644 --- a/internal/events/token_pool_created_test.go +++ b/internal/events/token_pool_created_test.go @@ -56,6 +56,29 @@ func TestTokenPoolCreatedIgnore(t *testing.T) { 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() diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go index f8abab72fb..dc863bedc0 100644 --- a/internal/tokens/fftokens/fftokens.go +++ b/internal/tokens/fftokens/fftokens.go @@ -174,23 +174,24 @@ func (ft *FFTokens) handleTokenPoolCreate(ctx context.Context, data fftypes.JSON protocolID := data.GetString("poolId") standard := data.GetString("standard") // this is optional operatorAddress := data.GetString("operator") - poolDataString := data.GetString("data") tx := data.GetObject("transaction") txHash := tx.GetString("transactionHash") if tokenType == "" || protocolID == "" || - poolDataString == "" || operatorAddress == "" || txHash == "" { log.L(ctx).Errorf("TokenPool event is not valid - missing data: %+v", 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).Errorf("TokenPool event is not valid - failed to parse data (%s): %+v", err, data) - return nil // move on + log.L(ctx).Infof("TokenPool event data could not be parsed - continuing anyway (%s): %+v", err, data) + poolData = tokenData{} } pool := &tokens.TokenPool{ @@ -236,12 +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. + // 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{ diff --git a/internal/tokens/fftokens/fftokens_test.go b/internal/tokens/fftokens/fftokens_test.go index bc36eefe49..0fd9193f3a 100644 --- a/internal/tokens/fftokens/fftokens_test.go +++ b/internal/tokens/fftokens/fftokens_test.go @@ -459,7 +459,10 @@ func TestEvents(t *testing.T) { msg = <-toServer assert.Equal(t, `{"data":{"id":"6"},"event":"ack"}`, string(msg)) - // token-pool: invalid uuid + // 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", @@ -478,7 +481,7 @@ func TestEvents(t *testing.T) { // token-pool: 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 == *txID + 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", diff --git a/pkg/tokens/plugin.go b/pkg/tokens/plugin.go index 2078b52eca..911b49f3e4 100644 --- a/pkg/tokens/plugin.go +++ b/pkg/tokens/plugin.go @@ -90,10 +90,22 @@ type Capabilities struct { // TokenPool is the set of data returned from the connector when a token pool is created. type TokenPool struct { - Type fftypes.TokenType - ProtocolID string + // 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 string - Connector string - Standard string + + // 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 } From 8b88db9ca8bf5e1aabd45f1b25708f5d4a37e4b4 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 18 Nov 2021 20:06:40 -0500 Subject: [PATCH 11/13] Update token connector in manifest Signed-off-by: Peter Broadhurst --- manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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" } } From 972d1f53eccd293cc63df549d4d4fed328c9b073 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 19 Nov 2021 09:09:55 -0500 Subject: [PATCH 12/13] E2E tests should check message types explicitly on events Signed-off-by: Peter Broadhurst --- test/e2e/e2e_test.go | 11 +++++++++++ test/e2e/onchain_offchain_test.go | 28 ++++++++++++++-------------- test/e2e/run.sh | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) 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 From 893d403b77a9a202b2ed90472380ea59cc5c5e1b Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Fri, 19 Nov 2021 10:38:42 -0500 Subject: [PATCH 13/13] Tweak helper naming in token_pool_created Signed-off-by: Andrew Richardson --- internal/events/token_pool_created.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go index d4ff8f0680..33d2d5b05a 100644 --- a/internal/events/token_pool_created.go +++ b/internal/events/token_pool_created.go @@ -26,19 +26,18 @@ import ( "github.com/hyperledger/firefly/pkg/tokens" ) -func updatePool(storedPool *fftypes.TokenPool, chainPool *tokens.TokenPool) *fftypes.TokenPool { - storedPool.Type = chainPool.Type - storedPool.ProtocolID = chainPool.ProtocolID - storedPool.Key = chainPool.Key - storedPool.Connector = chainPool.Connector - storedPool.Standard = chainPool.Standard - if chainPool.TransactionID != nil { - storedPool.TX = fftypes.TransactionRef{ +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: chainPool.TransactionID, + ID: pluginPool.TransactionID, } } - return storedPool } func poolTransaction(pool *fftypes.TokenPool, status fftypes.OpStatus, protocolTxID string, additionalInfo fftypes.JSONObject) *fftypes.Transaction { @@ -89,7 +88,7 @@ func (em *eventManager) shouldConfirm(ctx context.Context, pool *tokens.TokenPoo if existingPool, err = em.database.GetTokenPoolByProtocolID(ctx, pool.Connector, pool.ProtocolID); err != nil || existingPool == nil { return existingPool, err } - updatePool(existingPool, pool) + addPoolDetailsFromPlugin(existingPool, pool) if existingPool.State == fftypes.TokenPoolStateUnknown { // Unknown pool state - should only happen on first run after database migration @@ -118,12 +117,14 @@ func (em *eventManager) shouldAnnounce(ctx context.Context, ti tokens.Plugin, po } else if op == nil { return nil, nil } - announcePool = updatePool(&fftypes.TokenPool{}, pool) + 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,