From fa927e5210553d392df1a3a76dfc144b17273c4e Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Fri, 15 Oct 2021 15:44:22 -0400 Subject: [PATCH] Split asset manager into multiple files Signed-off-by: Andrew Richardson --- internal/assets/manager.go | 374 +--------- internal/assets/manager_test.go | 947 ------------------------- internal/assets/token_pool.go | 154 ++++ internal/assets/token_pool_test.go | 243 +++++++ internal/assets/token_transfer.go | 270 +++++++ internal/assets/token_transfer_test.go | 769 ++++++++++++++++++++ 6 files changed, 1437 insertions(+), 1320 deletions(-) create mode 100644 internal/assets/token_pool.go create mode 100644 internal/assets/token_pool_test.go create mode 100644 internal/assets/token_transfer.go create mode 100644 internal/assets/token_transfer_test.go diff --git a/internal/assets/manager.go b/internal/assets/manager.go index 33f9bd6ded..dd3294f981 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -18,7 +18,6 @@ package assets import ( "context" - "fmt" "github.com/hyperledger/firefly/internal/broadcast" "github.com/hyperledger/firefly/internal/config" @@ -39,8 +38,8 @@ type Manager interface { CreateTokenPool(ctx context.Context, ns, typeName string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) GetTokenPools(ctx context.Context, ns, typeName string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) GetTokenPool(ctx context.Context, ns, typeName, poolName string) (*fftypes.TokenPool, error) - GetTokenAccounts(ctx context.Context, ns, typeName, poolName string, filter database.AndFilter) ([]*fftypes.TokenAccount, *database.FilterResult, error) ValidateTokenPoolTx(ctx context.Context, pool *fftypes.TokenPool, protocolTxID string) error + GetTokenAccounts(ctx context.Context, ns, typeName, poolName string, filter database.AndFilter) ([]*fftypes.TokenAccount, *database.FilterResult, error) GetTokenTransfers(ctx context.Context, ns, typeName, poolName string, filter database.AndFilter) ([]*fftypes.TokenTransfer, *database.FilterResult, error) NewTransfer(ns, typeName, poolName string, transfer *fftypes.TokenTransferInput) sysmessaging.MessageSender MintTokens(ctx context.Context, ns, typeName, poolName string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (*fftypes.TokenTransfer, error) @@ -99,140 +98,10 @@ func (am *assetManager) selectTokenPlugin(ctx context.Context, name string) (tok return nil, i18n.NewError(ctx, i18n.MsgUnknownTokensPlugin, name) } -func addTokenPoolCreateInputs(op *fftypes.Operation, pool *fftypes.TokenPool) { - op.Input = fftypes.JSONObject{ - "id": pool.ID.String(), - "namespace": pool.Namespace, - "name": pool.Name, - "config": pool.Config, - } -} - -func retrieveTokenPoolCreateInputs(ctx context.Context, op *fftypes.Operation, pool *fftypes.TokenPool) (err error) { - input := &op.Input - pool.ID, err = fftypes.ParseUUID(ctx, input.GetString("id")) - if err != nil { - return err - } - pool.Namespace = input.GetString("namespace") - pool.Name = input.GetString("name") - if pool.Namespace == "" || pool.Name == "" { - return fmt.Errorf("namespace or name missing from inputs") - } - pool.Config = input.GetObject("config") - return nil -} - -// Note: the counterpart to below (retrieveTokenTransferInputs) lives in the events package -func addTokenTransferInputs(op *fftypes.Operation, transfer *fftypes.TokenTransfer) { - op.Input = fftypes.JSONObject{ - "id": transfer.LocalID.String(), - } -} - -func (am *assetManager) CreateTokenPool(ctx context.Context, ns string, typeName string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) { - return am.createTokenPoolWithID(ctx, fftypes.NewUUID(), ns, typeName, pool, waitConfirm) -} - -func (am *assetManager) createTokenPoolWithID(ctx context.Context, id *fftypes.UUID, ns string, typeName string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) { - if err := am.data.VerifyNamespaceExists(ctx, ns); err != nil { - return nil, err - } - - if pool.Key == "" { - org, err := am.identity.GetLocalOrganization(ctx) - if err != nil { - return nil, err - } - pool.Key = org.Identity - } - - plugin, err := am.selectTokenPlugin(ctx, typeName) - if err != nil { - return nil, err - } - - if waitConfirm { - requestID := fftypes.NewUUID() - return am.syncasync.SendConfirmTokenPool(ctx, ns, requestID, func(ctx context.Context) error { - _, err := am.createTokenPoolWithID(ctx, requestID, ns, typeName, pool, false) - return err - }) - } - - tx := &fftypes.Transaction{ - ID: fftypes.NewUUID(), - Subject: fftypes.TransactionSubject{ - Namespace: ns, - Type: fftypes.TransactionTypeTokenPool, - Signer: pool.Key, - Reference: id, - }, - Created: fftypes.Now(), - Status: fftypes.OpStatusPending, - } - tx.Hash = tx.Subject.Hash() - err = am.database.UpsertTransaction(ctx, tx, false /* should be new, or idempotent replay */) - if err != nil { - return nil, err - } - - pool.ID = id - pool.Namespace = ns - pool.TX.ID = tx.ID - pool.TX.Type = tx.Subject.Type - - op := fftypes.NewTXOperation( - plugin, - ns, - tx.ID, - "", - fftypes.OpTypeTokenCreatePool, - fftypes.OpStatusPending, - "") - addTokenPoolCreateInputs(op, pool) - err = am.database.UpsertOperation(ctx, op, false) - if err != nil { - return nil, err - } - - return pool, plugin.CreateTokenPool(ctx, op.ID, pool) -} - func (am *assetManager) scopeNS(ns string, filter database.AndFilter) database.AndFilter { return filter.Condition(filter.Builder().Eq("namespace", ns)) } -func (am *assetManager) GetTokenPools(ctx context.Context, ns string, typeName string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) { - if _, err := am.selectTokenPlugin(ctx, typeName); err != nil { - return nil, nil, err - } - if err := fftypes.ValidateFFNameField(ctx, ns, "namespace"); err != nil { - return nil, nil, err - } - return am.database.GetTokenPools(ctx, am.scopeNS(ns, filter)) -} - -func (am *assetManager) GetTokenPool(ctx context.Context, ns, typeName, poolName string) (*fftypes.TokenPool, error) { - if _, err := am.selectTokenPlugin(ctx, typeName); err != nil { - return nil, err - } - if err := fftypes.ValidateFFNameField(ctx, ns, "namespace"); err != nil { - return nil, err - } - if err := fftypes.ValidateFFNameField(ctx, poolName, "name"); err != nil { - return nil, err - } - pool, err := am.database.GetTokenPool(ctx, ns, poolName) - if err != nil { - return nil, err - } - if pool == nil { - return nil, i18n.NewError(ctx, i18n.Msg404NotFound) - } - return pool, nil -} - func (am *assetManager) GetTokenAccounts(ctx context.Context, ns, typeName, poolName string, filter database.AndFilter) ([]*fftypes.TokenAccount, *database.FilterResult, error) { pool, err := am.GetTokenPool(ctx, ns, typeName, poolName) if err != nil { @@ -241,247 +110,6 @@ func (am *assetManager) GetTokenAccounts(ctx context.Context, ns, typeName, pool return am.database.GetTokenAccounts(ctx, filter.Condition(filter.Builder().Eq("poolprotocolid", pool.ProtocolID))) } -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 -} - -func (am *assetManager) GetTokenTransfers(ctx context.Context, ns, typeName, name string, filter database.AndFilter) ([]*fftypes.TokenTransfer, *database.FilterResult, error) { - pool, err := am.GetTokenPool(ctx, ns, typeName, name) - if err != nil { - return nil, nil, err - } - return am.database.GetTokenTransfers(ctx, filter.Condition(filter.Builder().Eq("poolprotocolid", pool.ProtocolID))) -} - -func (am *assetManager) NewTransfer(ns, typeName, poolName string, transfer *fftypes.TokenTransferInput) sysmessaging.MessageSender { - sender := &transferSender{ - mgr: am, - namespace: ns, - typeName: typeName, - poolName: poolName, - transfer: transfer, - } - sender.setDefaults() - return sender -} - -type transferSender struct { - mgr *assetManager - namespace string - typeName string - poolName string - transfer *fftypes.TokenTransferInput - sendCallback sysmessaging.BeforeSendCallback -} - -func (s *transferSender) Send(ctx context.Context) error { - return s.resolveAndSend(ctx, false) -} - -func (s *transferSender) SendAndWait(ctx context.Context) error { - return s.resolveAndSend(ctx, true) -} - -func (s *transferSender) BeforeSend(cb sysmessaging.BeforeSendCallback) sysmessaging.MessageSender { - s.sendCallback = cb - return s -} - -func (s *transferSender) setDefaults() { - s.transfer.LocalID = fftypes.NewUUID() -} - -func (am *assetManager) MintTokens(ctx context.Context, ns, typeName, poolName string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (out *fftypes.TokenTransfer, err error) { - transfer.Type = fftypes.TokenTransferTypeMint - if transfer.Key == "" { - org, err := am.identity.GetLocalOrganization(ctx) - if err != nil { - return nil, err - } - transfer.Key = org.Identity - } - transfer.From = "" - if transfer.To == "" { - transfer.To = transfer.Key - } - - sender := am.NewTransfer(ns, typeName, poolName, transfer) - if waitConfirm { - err = sender.SendAndWait(ctx) - } else { - err = sender.Send(ctx) - } - return &transfer.TokenTransfer, err -} - -func (am *assetManager) BurnTokens(ctx context.Context, ns, typeName, poolName string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (out *fftypes.TokenTransfer, err error) { - transfer.Type = fftypes.TokenTransferTypeBurn - if transfer.Key == "" { - org, err := am.identity.GetLocalOrganization(ctx) - if err != nil { - return nil, err - } - transfer.Key = org.Identity - } - if transfer.From == "" { - transfer.From = transfer.Key - } - transfer.To = "" - - sender := am.NewTransfer(ns, typeName, poolName, transfer) - if waitConfirm { - err = sender.SendAndWait(ctx) - } else { - err = sender.Send(ctx) - } - return &transfer.TokenTransfer, err -} - -func (am *assetManager) TransferTokens(ctx context.Context, ns, typeName, poolName string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (out *fftypes.TokenTransfer, err error) { - transfer.Type = fftypes.TokenTransferTypeTransfer - if transfer.Key == "" { - org, err := am.identity.GetLocalOrganization(ctx) - if err != nil { - return nil, err - } - transfer.Key = org.Identity - } - if transfer.From == "" { - transfer.From = transfer.Key - } - if transfer.To == "" { - transfer.To = transfer.Key - } - if transfer.From == transfer.To { - return nil, i18n.NewError(ctx, i18n.MsgCannotTransferToSelf) - } - - sender := am.NewTransfer(ns, typeName, poolName, transfer) - if waitConfirm { - err = sender.SendAndWait(ctx) - } else { - err = sender.Send(ctx) - } - return &transfer.TokenTransfer, err -} - -func (s *transferSender) resolveAndSend(ctx context.Context, waitConfirm bool) (err error) { - plugin, err := s.mgr.selectTokenPlugin(ctx, s.typeName) - if err != nil { - return err - } - pool, err := s.mgr.GetTokenPool(ctx, s.namespace, s.typeName, s.poolName) - if err != nil { - return err - } - s.transfer.PoolProtocolID = pool.ProtocolID - - var messageSender sysmessaging.MessageSender - if s.transfer.Message != nil { - if messageSender, err = s.buildTransferMessage(ctx, s.namespace, s.transfer.Message); err != nil { - return err - } - } - - switch { - case waitConfirm && messageSender != nil: - // prepare the message, send the transfer async, then send the message and wait - return messageSender. - BeforeSend(func(ctx context.Context) error { - s.transfer.MessageHash = s.transfer.Message.Hash - s.transfer.Message = nil - return s.Send(ctx) - }). - SendAndWait(ctx) - case waitConfirm: - // no message - just send the transfer and wait - return s.sendSync(ctx) - case messageSender != nil: - // send the message async and then move on to the transfer - if err := messageSender.Send(ctx); err != nil { - return err - } - s.transfer.MessageHash = s.transfer.Message.Hash - } - - tx := &fftypes.Transaction{ - ID: fftypes.NewUUID(), - Subject: fftypes.TransactionSubject{ - Namespace: s.namespace, - Type: fftypes.TransactionTypeTokenTransfer, - Signer: s.transfer.Key, - Reference: s.transfer.LocalID, - }, - Created: fftypes.Now(), - Status: fftypes.OpStatusPending, - } - tx.Hash = tx.Subject.Hash() - err = s.mgr.database.UpsertTransaction(ctx, tx, false /* should be new, or idempotent replay */) - if err != nil { - return err - } - - s.transfer.TX.ID = tx.ID - s.transfer.TX.Type = tx.Subject.Type - - op := fftypes.NewTXOperation( - plugin, - s.namespace, - tx.ID, - "", - fftypes.OpTypeTokenTransfer, - fftypes.OpStatusPending, - "") - addTokenTransferInputs(op, &s.transfer.TokenTransfer) - if err := s.mgr.database.UpsertOperation(ctx, op, false); err != nil { - return err - } - - if s.sendCallback != nil { - if err := s.sendCallback(ctx); err != nil { - return err - } - } - - switch s.transfer.Type { - case fftypes.TokenTransferTypeMint: - return plugin.MintTokens(ctx, op.ID, &s.transfer.TokenTransfer) - case fftypes.TokenTransferTypeTransfer: - return plugin.TransferTokens(ctx, op.ID, &s.transfer.TokenTransfer) - case fftypes.TokenTransferTypeBurn: - return plugin.BurnTokens(ctx, op.ID, &s.transfer.TokenTransfer) - default: - panic(fmt.Sprintf("unknown transfer type: %v", s.transfer.Type)) - } -} - -func (s *transferSender) sendSync(ctx context.Context) error { - out, err := s.mgr.syncasync.SendConfirmTokenTransfer(ctx, s.namespace, s.transfer.LocalID, s.Send) - if out != nil { - s.transfer.TokenTransfer = *out - } - return err -} - -func (s *transferSender) buildTransferMessage(ctx context.Context, ns string, in *fftypes.MessageInOut) (sysmessaging.MessageSender, error) { - allowedTypes := []fftypes.FFEnum{ - fftypes.MessageTypeTransferBroadcast, - fftypes.MessageTypeTransferPrivate, - } - if in.Header.Type == "" { - in.Header.Type = fftypes.MessageTypeTransferBroadcast - } - switch in.Header.Type { - case fftypes.MessageTypeTransferBroadcast: - return s.mgr.broadcast.NewBroadcast(ns, in), nil - case fftypes.MessageTypeTransferPrivate: - return s.mgr.messaging.NewMessage(ns, in), nil - default: - return nil, i18n.NewError(ctx, i18n.MsgInvalidMessageType, allowedTypes) - } -} - func (am *assetManager) Start() error { return nil } diff --git a/internal/assets/manager_test.go b/internal/assets/manager_test.go index 09ee71c88b..1c75ee3adf 100644 --- a/internal/assets/manager_test.go +++ b/internal/assets/manager_test.go @@ -20,15 +20,12 @@ import ( "testing" "github.com/hyperledger/firefly/internal/config" - "github.com/hyperledger/firefly/internal/syncasync" - "github.com/hyperledger/firefly/internal/sysmessaging" "github.com/hyperledger/firefly/mocks/broadcastmocks" "github.com/hyperledger/firefly/mocks/databasemocks" "github.com/hyperledger/firefly/mocks/datamocks" "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/mocks/privatemessagingmocks" "github.com/hyperledger/firefly/mocks/syncasyncmocks" - "github.com/hyperledger/firefly/mocks/sysmessagingmocks" "github.com/hyperledger/firefly/mocks/tokenmocks" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" @@ -70,209 +67,6 @@ func TestStartStop(t *testing.T) { am.WaitStop() } -func TestCreateTokenPoolBadNamespace(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdm := am.data.(*datamocks.Manager) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(fmt.Errorf("pop")) - - _, err := am.CreateTokenPool(context.Background(), "ns1", "test", &fftypes.TokenPool{}, false) - assert.EqualError(t, err, "pop") -} - -func TestCreateTokenPoolIdentityFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdm := am.data.(*datamocks.Manager) - mim := am.identity.(*identitymanagermocks.Manager) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mim.On("GetLocalOrganization", context.Background()).Return(nil, fmt.Errorf("pop")) - - _, err := am.CreateTokenPool(context.Background(), "ns1", "test", &fftypes.TokenPool{}, false) - assert.EqualError(t, err, "pop") -} - -func TestCreateTokenPoolBadConnector(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdm := am.data.(*datamocks.Manager) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - - _, err := am.CreateTokenPool(context.Background(), "ns1", "bad", &fftypes.TokenPool{}, false) - assert.Regexp(t, "FF10272", err) -} - -func TestCreateTokenPoolFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdi := am.database.(*databasemocks.Plugin) - mdm := am.data.(*datamocks.Manager) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenPool - }), false).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) - - _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, false) - assert.Regexp(t, "pop", err) -} - -func TestCreateTokenPoolTransactionFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdi := am.database.(*databasemocks.Plugin) - mdm := am.data.(*datamocks.Manager) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.Anything, false).Return(fmt.Errorf("pop")) - - _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, false) - assert.Regexp(t, "pop", err) -} - -func TestCreateTokenPoolOperationFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdi := am.database.(*databasemocks.Plugin) - mdm := am.data.(*datamocks.Manager) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenPool - }), false).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(fmt.Errorf("pop")) - - _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, false) - assert.Regexp(t, "pop", err) -} - -func TestCreateTokenPoolSuccess(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdi := am.database.(*databasemocks.Plugin) - mdm := am.data.(*datamocks.Manager) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenPool - }), false).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - - _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, false) - assert.NoError(t, err) -} - -func TestCreateTokenPoolConfirm(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdi := am.database.(*databasemocks.Plugin) - mdm := am.data.(*datamocks.Manager) - msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil).Times(2) - mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything).Return(nil).Times(1) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenPool - }), false).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil).Times(1) - msa.On("SendConfirmTokenPool", context.Background(), "ns1", mock.Anything, mock.Anything). - Run(func(args mock.Arguments) { - send := args[3].(syncasync.RequestSender) - send(context.Background()) - }). - Return(nil, nil) - - _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, true) - assert.NoError(t, err) -} - -func TestGetTokenPool(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdi := am.database.(*databasemocks.Plugin) - mdi.On("GetTokenPool", context.Background(), "ns1", "abc").Return(&fftypes.TokenPool{}, nil) - _, err := am.GetTokenPool(context.Background(), "ns1", "magic-tokens", "abc") - assert.NoError(t, err) -} - -func TestGetTokenPoolBadPlugin(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - _, err := am.GetTokenPool(context.Background(), "", "", "") - assert.Regexp(t, "FF10272", err) -} - -func TestGetTokenPoolBadNamespace(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - _, err := am.GetTokenPool(context.Background(), "", "magic-tokens", "") - assert.Regexp(t, "FF10131", err) -} - -func TestGetTokenPoolBadName(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - _, err := am.GetTokenPool(context.Background(), "ns1", "magic-tokens", "") - assert.Regexp(t, "FF10131", err) -} - -func TestGetTokenPools(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - u := fftypes.NewUUID() - mdi := am.database.(*databasemocks.Plugin) - fb := database.TokenPoolQueryFactory.NewFilter(context.Background()) - f := fb.And(fb.Eq("id", u)) - mdi.On("GetTokenPools", context.Background(), f).Return([]*fftypes.TokenPool{}, nil, nil) - _, _, err := am.GetTokenPools(context.Background(), "ns1", "magic-tokens", f) - assert.NoError(t, err) -} - -func TestGetTokenPoolsBadPlugin(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - _, _, err := am.GetTokenPools(context.Background(), "", "", nil) - assert.Regexp(t, "FF10272", err) -} - -func TestGetTokenPoolsBadNamespace(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - u := fftypes.NewUUID() - fb := database.TokenPoolQueryFactory.NewFilter(context.Background()) - f := fb.And(fb.Eq("id", u)) - _, _, err := am.GetTokenPools(context.Background(), "", "magic-tokens", f) - assert.Regexp(t, "FF10131", err) -} - func TestGetTokenAccounts(t *testing.T) { am, cancel := newTestAssets(t) defer cancel() @@ -300,744 +94,3 @@ func TestGetTokenAccountsBadPool(t *testing.T) { _, _, err := am.GetTokenAccounts(context.Background(), "ns1", "magic-tokens", "test", f) assert.EqualError(t, err, "pop") } - -func TestValidateTokenPoolTx(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - err := am.ValidateTokenPoolTx(context.Background(), nil, "") - assert.NoError(t, err) -} - -func TestGetTokenTransfers(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - pool := &fftypes.TokenPool{ - ID: fftypes.NewUUID(), - } - mdi := am.database.(*databasemocks.Plugin) - fb := database.TokenTransferQueryFactory.NewFilter(context.Background()) - f := fb.And() - mdi.On("GetTokenPool", context.Background(), "ns1", "test").Return(pool, nil) - mdi.On("GetTokenTransfers", context.Background(), f).Return([]*fftypes.TokenTransfer{}, nil, nil) - _, _, err := am.GetTokenTransfers(context.Background(), "ns1", "magic-tokens", "test", f) - assert.NoError(t, err) -} - -func TestGetTokenTransfersBadPool(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdi := am.database.(*databasemocks.Plugin) - fb := database.TokenTransferQueryFactory.NewFilter(context.Background()) - f := fb.And() - mdi.On("GetTokenPool", context.Background(), "ns1", "test").Return(nil, fmt.Errorf("pop")) - _, _, err := am.GetTokenTransfers(context.Background(), "ns1", "magic-tokens", "test", f) - assert.EqualError(t, err, "pop") -} - -func TestGetTokenTransfersNoPool(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mdi := am.database.(*databasemocks.Plugin) - fb := database.TokenTransferQueryFactory.NewFilter(context.Background()) - f := fb.And() - mdi.On("GetTokenPool", context.Background(), "ns1", "test").Return(nil, nil) - _, _, err := am.GetTokenTransfers(context.Background(), "ns1", "magic-tokens", "test", f) - assert.Regexp(t, "FF10109", err) -} - -func TestMintTokensSuccess(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mint := &fftypes.TokenTransferInput{} - mint.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.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(&fftypes.TokenPool{}, nil) - mti.On("MintTokens", context.Background(), mock.Anything, &mint.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - - _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) - assert.NoError(t, err) -} - -func TestMintTokensBadPlugin(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - - _, err := am.MintTokens(context.Background(), "", "", "", &fftypes.TokenTransferInput{}, false) - assert.Regexp(t, "FF10272", err) -} - -func TestMintTokensBadPool(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mint := &fftypes.TokenTransferInput{} - mint.Amount.Int().SetInt64(5) - - 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(nil, fmt.Errorf("pop")) - - _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) - assert.EqualError(t, err, "pop") -} - -func TestMintTokensIdentityFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mint := &fftypes.TokenTransferInput{} - mint.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(nil, fmt.Errorf("pop")) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - - _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) - assert.EqualError(t, err, "pop") -} - -func TestMintTokensFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mint := &fftypes.TokenTransferInput{} - mint.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.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(&fftypes.TokenPool{}, nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mti.On("MintTokens", context.Background(), mock.Anything, &mint.TokenTransfer).Return(fmt.Errorf("pop")) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - - _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) - assert.EqualError(t, err, "pop") -} - -func TestMintTokensOperationFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mint := &fftypes.TokenTransferInput{} - mint.Amount.Int().SetInt64(5) - - 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(&fftypes.TokenPool{}, nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(fmt.Errorf("pop")) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - - _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) - assert.EqualError(t, err, "pop") -} - -func TestMintTokensConfirm(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - mint := &fftypes.TokenTransferInput{} - mint.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mdm := am.data.(*datamocks.Manager) - msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mti.On("MintTokens", context.Background(), mock.Anything, &mint.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - msa.On("SendConfirmTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). - Run(func(args mock.Arguments) { - send := args[3].(syncasync.RequestSender) - send(context.Background()) - }). - Return(&fftypes.TokenTransfer{}, nil) - - _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, true) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mdm.AssertExpectations(t) - msa.AssertExpectations(t) - mti.AssertExpectations(t) -} - -func TestBurnTokensSuccess(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - burn := &fftypes.TokenTransferInput{} - burn.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.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(&fftypes.TokenPool{}, nil) - mti.On("BurnTokens", context.Background(), mock.Anything, &burn.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - - _, err := am.BurnTokens(context.Background(), "ns1", "magic-tokens", "pool1", burn, false) - assert.NoError(t, err) - - mim.AssertExpectations(t) - mdi.AssertExpectations(t) - mti.AssertExpectations(t) -} - -func TestBurnTokensIdentityFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - burn := &fftypes.TokenTransferInput{} - burn.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(nil, fmt.Errorf("pop")) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - - _, err := am.BurnTokens(context.Background(), "ns1", "magic-tokens", "pool1", burn, false) - assert.EqualError(t, err, "pop") -} - -func TestBurnTokensConfirm(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - burn := &fftypes.TokenTransferInput{} - burn.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mdm := am.data.(*datamocks.Manager) - msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mti.On("BurnTokens", context.Background(), mock.Anything, &burn.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - msa.On("SendConfirmTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). - Run(func(args mock.Arguments) { - send := args[3].(syncasync.RequestSender) - send(context.Background()) - }). - Return(&fftypes.TokenTransfer{}, nil) - - _, err := am.BurnTokens(context.Background(), "ns1", "magic-tokens", "pool1", burn, true) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mdm.AssertExpectations(t) - msa.AssertExpectations(t) - mti.AssertExpectations(t) -} - -func TestTransferTokensSuccess(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.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(&fftypes.TokenPool{}, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) - assert.NoError(t, err) - - mim.AssertExpectations(t) - mdi.AssertExpectations(t) - mti.AssertExpectations(t) -} - -func TestTransferTokensIdentityFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(nil, fmt.Errorf("pop")) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) - assert.EqualError(t, err, "pop") -} - -func TestTransferTokensNoFromOrTo(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{} - - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) - assert.Regexp(t, "FF10280", err) - - mim.AssertExpectations(t) -} - -func TestTransferTokensInvalidType(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mdi.On("GetTokenPool", am.ctx, "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - mdi.On("UpsertOperation", am.ctx, mock.Anything, false).Return(nil) - - sender := &transferSender{ - mgr: am, - namespace: "ns1", - typeName: "magic-tokens", - poolName: "pool1", - transfer: transfer, - } - assert.PanicsWithValue(t, "unknown transfer type: ", func() { - sender.Send(am.ctx) - }) -} - -func TestTransferTokensTransactionFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - } - transfer.Amount.Int().SetInt64(5) - - 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(&fftypes.TokenPool{}, nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(fmt.Errorf("pop")) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) - assert.EqualError(t, err, "pop") - - mim.AssertExpectations(t) - mdi.AssertExpectations(t) -} - -func TestTransferTokensWithBroadcastMessage(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - hash := fftypes.NewRandB32() - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - Message: &fftypes.MessageInOut{ - Message: fftypes.Message{ - Hash: hash, - }, - InlineData: fftypes.InlineData{ - { - Value: []byte("test data"), - }, - }, - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mbm := am.broadcast.(*broadcastmocks.Manager) - mms := &sysmessagingmocks.MessageSender{} - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) - mms.On("Send", context.Background()).Return(nil) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) - assert.NoError(t, err) - assert.Equal(t, *hash, *transfer.MessageHash) - - mbm.AssertExpectations(t) - mim.AssertExpectations(t) - mdi.AssertExpectations(t) - mti.AssertExpectations(t) - mms.AssertExpectations(t) -} - -func TestTransferTokensWithBroadcastMessageFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - Message: &fftypes.MessageInOut{ - InlineData: fftypes.InlineData{ - { - Value: []byte("test data"), - }, - }, - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mbm := am.broadcast.(*broadcastmocks.Manager) - mms := &sysmessagingmocks.MessageSender{} - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) - mms.On("Send", context.Background()).Return(fmt.Errorf("pop")) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) - assert.EqualError(t, err, "pop") - - mbm.AssertExpectations(t) - mim.AssertExpectations(t) - mdi.AssertExpectations(t) - mms.AssertExpectations(t) -} - -func TestTransferTokensWithPrivateMessage(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - hash := fftypes.NewRandB32() - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - Message: &fftypes.MessageInOut{ - Message: fftypes.Message{ - Header: fftypes.MessageHeader{ - Type: fftypes.MessageTypeTransferPrivate, - }, - Hash: hash, - }, - InlineData: fftypes.InlineData{ - { - Value: []byte("test data"), - }, - }, - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mpm := am.messaging.(*privatemessagingmocks.Manager) - mms := &sysmessagingmocks.MessageSender{} - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - mpm.On("NewMessage", "ns1", transfer.Message).Return(mms) - mms.On("Send", context.Background()).Return(nil) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) - assert.NoError(t, err) - assert.Equal(t, *hash, *transfer.MessageHash) - - mpm.AssertExpectations(t) - mim.AssertExpectations(t) - mdi.AssertExpectations(t) - mti.AssertExpectations(t) - mms.AssertExpectations(t) -} - -func TestTransferTokensWithInvalidMessage(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - Message: &fftypes.MessageInOut{ - Message: fftypes.Message{ - Header: fftypes.MessageHeader{ - Type: fftypes.MessageTypeDefinition, - }, - }, - InlineData: fftypes.InlineData{ - { - Value: []byte("test data"), - }, - }, - }, - } - transfer.Amount.Int().SetInt64(5) - - 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(&fftypes.TokenPool{}, nil) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) - assert.Regexp(t, "FF10287", err) - - mim.AssertExpectations(t) - mdi.AssertExpectations(t) -} - -func TestTransferTokensConfirm(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mdm := am.data.(*datamocks.Manager) - msa := am.syncasync.(*syncasyncmocks.Bridge) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - msa.On("SendConfirmTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). - Run(func(args mock.Arguments) { - send := args[3].(syncasync.RequestSender) - send(context.Background()) - }). - Return(&fftypes.TokenTransfer{}, nil) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, true) - assert.NoError(t, err) - - mdi.AssertExpectations(t) - mdm.AssertExpectations(t) - msa.AssertExpectations(t) - mti.AssertExpectations(t) -} - -func TestTransferTokensBeforeSendCallback(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - Type: fftypes.TokenTransferTypeTransfer, - From: "A", - To: "B", - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - - sender := am.NewTransfer("ns1", "magic-tokens", "pool1", transfer) - - called := false - sender.BeforeSend(func(ctx context.Context) error { - called = true - return nil - }) - - err := sender.Send(context.Background()) - assert.NoError(t, err) - assert.True(t, called) - - mim.AssertExpectations(t) - mdi.AssertExpectations(t) - mti.AssertExpectations(t) -} - -func TestTransferTokensBeforeSendCallbackFail(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - Type: fftypes.TokenTransferTypeTransfer, - From: "A", - To: "B", - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - - sender := am.NewTransfer("ns1", "magic-tokens", "pool1", transfer) - - called := false - sender.BeforeSend(func(ctx context.Context) error { - called = true - return fmt.Errorf("pop") - }) - - err := sender.Send(context.Background()) - assert.EqualError(t, err, "pop") - assert.True(t, called) - - mim.AssertExpectations(t) - mdi.AssertExpectations(t) - mti.AssertExpectations(t) -} - -func TestTransferTokensWithBroadcastMessageConfirm(t *testing.T) { - am, cancel := newTestAssets(t) - defer cancel() - - hash := fftypes.NewRandB32() - transfer := &fftypes.TokenTransferInput{ - TokenTransfer: fftypes.TokenTransfer{ - From: "A", - To: "B", - }, - Message: &fftypes.MessageInOut{ - Message: fftypes.Message{ - Hash: hash, - }, - InlineData: fftypes.InlineData{ - { - Value: []byte("test data"), - }, - }, - }, - } - transfer.Amount.Int().SetInt64(5) - - mdi := am.database.(*databasemocks.Plugin) - mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) - mim := am.identity.(*identitymanagermocks.Manager) - mbm := am.broadcast.(*broadcastmocks.Manager) - mms := &sysmessagingmocks.MessageSender{} - mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) - mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) - mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) - mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) - mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { - return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer - }), false).Return(nil) - mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) - mms.On("BeforeSend", mock.Anything). - Run(func(args mock.Arguments) { - cb := args[0].(sysmessaging.BeforeSendCallback) - cb(context.Background()) - }). - Return(mms) - mms.On("SendAndWait", context.Background()).Return(nil) - - _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, true) - assert.NoError(t, err) - assert.Nil(t, transfer.Message) - assert.Equal(t, *hash, *transfer.MessageHash) - - mbm.AssertExpectations(t) - mim.AssertExpectations(t) - mdi.AssertExpectations(t) - mti.AssertExpectations(t) - mms.AssertExpectations(t) -} diff --git a/internal/assets/token_pool.go b/internal/assets/token_pool.go new file mode 100644 index 0000000000..d8159a1181 --- /dev/null +++ b/internal/assets/token_pool.go @@ -0,0 +1,154 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package assets + +import ( + "context" + "fmt" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +func addTokenPoolCreateInputs(op *fftypes.Operation, pool *fftypes.TokenPool) { + op.Input = fftypes.JSONObject{ + "id": pool.ID.String(), + "namespace": pool.Namespace, + "name": pool.Name, + "config": pool.Config, + } +} + +func retrieveTokenPoolCreateInputs(ctx context.Context, op *fftypes.Operation, pool *fftypes.TokenPool) (err error) { + input := &op.Input + pool.ID, err = fftypes.ParseUUID(ctx, input.GetString("id")) + if err != nil { + return err + } + pool.Namespace = input.GetString("namespace") + pool.Name = input.GetString("name") + if pool.Namespace == "" || pool.Name == "" { + return fmt.Errorf("namespace or name missing from inputs") + } + pool.Config = input.GetObject("config") + return nil +} + +func (am *assetManager) CreateTokenPool(ctx context.Context, ns string, typeName string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) { + return am.createTokenPoolWithID(ctx, fftypes.NewUUID(), ns, typeName, pool, waitConfirm) +} + +func (am *assetManager) createTokenPoolWithID(ctx context.Context, id *fftypes.UUID, ns string, typeName string, pool *fftypes.TokenPool, waitConfirm bool) (*fftypes.TokenPool, error) { + if err := am.data.VerifyNamespaceExists(ctx, ns); err != nil { + return nil, err + } + + if pool.Key == "" { + org, err := am.identity.GetLocalOrganization(ctx) + if err != nil { + return nil, err + } + pool.Key = org.Identity + } + + plugin, err := am.selectTokenPlugin(ctx, typeName) + if err != nil { + return nil, err + } + + if waitConfirm { + requestID := fftypes.NewUUID() + return am.syncasync.SendConfirmTokenPool(ctx, ns, requestID, func(ctx context.Context) error { + _, err := am.createTokenPoolWithID(ctx, requestID, ns, typeName, pool, false) + return err + }) + } + + tx := &fftypes.Transaction{ + ID: fftypes.NewUUID(), + Subject: fftypes.TransactionSubject{ + Namespace: ns, + Type: fftypes.TransactionTypeTokenPool, + Signer: pool.Key, + Reference: id, + }, + Created: fftypes.Now(), + Status: fftypes.OpStatusPending, + } + tx.Hash = tx.Subject.Hash() + err = am.database.UpsertTransaction(ctx, tx, false /* should be new, or idempotent replay */) + if err != nil { + return nil, err + } + + pool.ID = id + pool.Namespace = ns + pool.TX.ID = tx.ID + pool.TX.Type = tx.Subject.Type + + op := fftypes.NewTXOperation( + plugin, + ns, + tx.ID, + "", + fftypes.OpTypeTokenCreatePool, + fftypes.OpStatusPending, + "") + addTokenPoolCreateInputs(op, pool) + err = am.database.UpsertOperation(ctx, op, false) + if err != nil { + return nil, err + } + + return pool, plugin.CreateTokenPool(ctx, op.ID, pool) +} + +func (am *assetManager) GetTokenPools(ctx context.Context, ns string, typeName string, filter database.AndFilter) ([]*fftypes.TokenPool, *database.FilterResult, error) { + if _, err := am.selectTokenPlugin(ctx, typeName); err != nil { + return nil, nil, err + } + if err := fftypes.ValidateFFNameField(ctx, ns, "namespace"); err != nil { + return nil, nil, err + } + return am.database.GetTokenPools(ctx, am.scopeNS(ns, filter)) +} + +func (am *assetManager) GetTokenPool(ctx context.Context, ns, typeName, poolName string) (*fftypes.TokenPool, error) { + if _, err := am.selectTokenPlugin(ctx, typeName); err != nil { + return nil, err + } + if err := fftypes.ValidateFFNameField(ctx, ns, "namespace"); err != nil { + return nil, err + } + if err := fftypes.ValidateFFNameField(ctx, poolName, "name"); err != nil { + return nil, err + } + pool, err := am.database.GetTokenPool(ctx, ns, poolName) + if err != nil { + return nil, err + } + if pool == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + } + 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_test.go b/internal/assets/token_pool_test.go new file mode 100644 index 0000000000..feefcf8bda --- /dev/null +++ b/internal/assets/token_pool_test.go @@ -0,0 +1,243 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package assets + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly/internal/syncasync" + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/datamocks" + "github.com/hyperledger/firefly/mocks/identitymanagermocks" + "github.com/hyperledger/firefly/mocks/syncasyncmocks" + "github.com/hyperledger/firefly/mocks/tokenmocks" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestCreateTokenPoolBadNamespace(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdm := am.data.(*datamocks.Manager) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(fmt.Errorf("pop")) + + _, err := am.CreateTokenPool(context.Background(), "ns1", "test", &fftypes.TokenPool{}, false) + assert.EqualError(t, err, "pop") +} + +func TestCreateTokenPoolIdentityFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdm := am.data.(*datamocks.Manager) + mim := am.identity.(*identitymanagermocks.Manager) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mim.On("GetLocalOrganization", context.Background()).Return(nil, fmt.Errorf("pop")) + + _, err := am.CreateTokenPool(context.Background(), "ns1", "test", &fftypes.TokenPool{}, false) + assert.EqualError(t, err, "pop") +} + +func TestCreateTokenPoolBadConnector(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdm := am.data.(*datamocks.Manager) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + + _, err := am.CreateTokenPool(context.Background(), "ns1", "bad", &fftypes.TokenPool{}, false) + assert.Regexp(t, "FF10272", err) +} + +func TestCreateTokenPoolFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdi := am.database.(*databasemocks.Plugin) + mdm := am.data.(*datamocks.Manager) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenPool + }), false).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + + _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, false) + assert.Regexp(t, "pop", err) +} + +func TestCreateTokenPoolTransactionFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdi := am.database.(*databasemocks.Plugin) + mdm := am.data.(*datamocks.Manager) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.Anything, false).Return(fmt.Errorf("pop")) + + _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, false) + assert.Regexp(t, "pop", err) +} + +func TestCreateTokenPoolOperationFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdi := am.database.(*databasemocks.Plugin) + mdm := am.data.(*datamocks.Manager) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenPool + }), false).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(fmt.Errorf("pop")) + + _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, false) + assert.Regexp(t, "pop", err) +} + +func TestCreateTokenPoolSuccess(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdi := am.database.(*databasemocks.Plugin) + mdm := am.data.(*datamocks.Manager) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil) + mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenPool + }), false).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + + _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, false) + assert.NoError(t, err) +} + +func TestCreateTokenPoolConfirm(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdi := am.database.(*databasemocks.Plugin) + mdm := am.data.(*datamocks.Manager) + msa := am.syncasync.(*syncasyncmocks.Bridge) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdm.On("VerifyNamespaceExists", context.Background(), "ns1").Return(nil).Times(2) + mti.On("CreateTokenPool", context.Background(), mock.Anything, mock.Anything).Return(nil).Times(1) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenPool + }), false).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil).Times(1) + msa.On("SendConfirmTokenPool", context.Background(), "ns1", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + send := args[3].(syncasync.RequestSender) + send(context.Background()) + }). + Return(nil, nil) + + _, err := am.CreateTokenPool(context.Background(), "ns1", "magic-tokens", &fftypes.TokenPool{}, true) + assert.NoError(t, err) +} + +func TestGetTokenPool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdi := am.database.(*databasemocks.Plugin) + mdi.On("GetTokenPool", context.Background(), "ns1", "abc").Return(&fftypes.TokenPool{}, nil) + _, err := am.GetTokenPool(context.Background(), "ns1", "magic-tokens", "abc") + assert.NoError(t, err) +} + +func TestGetTokenPoolBadPlugin(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + _, err := am.GetTokenPool(context.Background(), "", "", "") + assert.Regexp(t, "FF10272", err) +} + +func TestGetTokenPoolBadNamespace(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + _, err := am.GetTokenPool(context.Background(), "", "magic-tokens", "") + assert.Regexp(t, "FF10131", err) +} + +func TestGetTokenPoolBadName(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + _, err := am.GetTokenPool(context.Background(), "ns1", "magic-tokens", "") + assert.Regexp(t, "FF10131", err) +} + +func TestGetTokenPools(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + u := fftypes.NewUUID() + mdi := am.database.(*databasemocks.Plugin) + fb := database.TokenPoolQueryFactory.NewFilter(context.Background()) + f := fb.And(fb.Eq("id", u)) + mdi.On("GetTokenPools", context.Background(), f).Return([]*fftypes.TokenPool{}, nil, nil) + _, _, err := am.GetTokenPools(context.Background(), "ns1", "magic-tokens", f) + assert.NoError(t, err) +} + +func TestGetTokenPoolsBadPlugin(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + _, _, err := am.GetTokenPools(context.Background(), "", "", nil) + assert.Regexp(t, "FF10272", err) +} + +func TestGetTokenPoolsBadNamespace(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + u := fftypes.NewUUID() + fb := database.TokenPoolQueryFactory.NewFilter(context.Background()) + f := fb.And(fb.Eq("id", u)) + _, _, err := am.GetTokenPools(context.Background(), "", "magic-tokens", f) + assert.Regexp(t, "FF10131", err) +} + +func TestValidateTokenPoolTx(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + err := am.ValidateTokenPoolTx(context.Background(), nil, "") + assert.NoError(t, err) +} diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go new file mode 100644 index 0000000000..960cf911b6 --- /dev/null +++ b/internal/assets/token_transfer.go @@ -0,0 +1,270 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package assets + +import ( + "context" + "fmt" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/internal/sysmessaging" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +// Note: the counterpart to below (retrieveTokenTransferInputs) lives in the events package +func addTokenTransferInputs(op *fftypes.Operation, transfer *fftypes.TokenTransfer) { + op.Input = fftypes.JSONObject{ + "id": transfer.LocalID.String(), + } +} + +func (am *assetManager) GetTokenTransfers(ctx context.Context, ns, typeName, name string, filter database.AndFilter) ([]*fftypes.TokenTransfer, *database.FilterResult, error) { + pool, err := am.GetTokenPool(ctx, ns, typeName, name) + if err != nil { + return nil, nil, err + } + return am.database.GetTokenTransfers(ctx, filter.Condition(filter.Builder().Eq("poolprotocolid", pool.ProtocolID))) +} + +func (am *assetManager) NewTransfer(ns, typeName, poolName string, transfer *fftypes.TokenTransferInput) sysmessaging.MessageSender { + sender := &transferSender{ + mgr: am, + namespace: ns, + typeName: typeName, + poolName: poolName, + transfer: transfer, + } + sender.setDefaults() + return sender +} + +type transferSender struct { + mgr *assetManager + namespace string + typeName string + poolName string + transfer *fftypes.TokenTransferInput + sendCallback sysmessaging.BeforeSendCallback +} + +func (s *transferSender) Send(ctx context.Context) error { + return s.resolveAndSend(ctx, false) +} + +func (s *transferSender) SendAndWait(ctx context.Context) error { + return s.resolveAndSend(ctx, true) +} + +func (s *transferSender) BeforeSend(cb sysmessaging.BeforeSendCallback) sysmessaging.MessageSender { + s.sendCallback = cb + return s +} + +func (s *transferSender) setDefaults() { + s.transfer.LocalID = fftypes.NewUUID() +} + +func (am *assetManager) MintTokens(ctx context.Context, ns, typeName, poolName string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (out *fftypes.TokenTransfer, err error) { + transfer.Type = fftypes.TokenTransferTypeMint + if transfer.Key == "" { + org, err := am.identity.GetLocalOrganization(ctx) + if err != nil { + return nil, err + } + transfer.Key = org.Identity + } + transfer.From = "" + if transfer.To == "" { + transfer.To = transfer.Key + } + + sender := am.NewTransfer(ns, typeName, poolName, transfer) + if waitConfirm { + err = sender.SendAndWait(ctx) + } else { + err = sender.Send(ctx) + } + return &transfer.TokenTransfer, err +} + +func (am *assetManager) BurnTokens(ctx context.Context, ns, typeName, poolName string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (out *fftypes.TokenTransfer, err error) { + transfer.Type = fftypes.TokenTransferTypeBurn + if transfer.Key == "" { + org, err := am.identity.GetLocalOrganization(ctx) + if err != nil { + return nil, err + } + transfer.Key = org.Identity + } + if transfer.From == "" { + transfer.From = transfer.Key + } + transfer.To = "" + + sender := am.NewTransfer(ns, typeName, poolName, transfer) + if waitConfirm { + err = sender.SendAndWait(ctx) + } else { + err = sender.Send(ctx) + } + return &transfer.TokenTransfer, err +} + +func (am *assetManager) TransferTokens(ctx context.Context, ns, typeName, poolName string, transfer *fftypes.TokenTransferInput, waitConfirm bool) (out *fftypes.TokenTransfer, err error) { + transfer.Type = fftypes.TokenTransferTypeTransfer + if transfer.Key == "" { + org, err := am.identity.GetLocalOrganization(ctx) + if err != nil { + return nil, err + } + transfer.Key = org.Identity + } + if transfer.From == "" { + transfer.From = transfer.Key + } + if transfer.To == "" { + transfer.To = transfer.Key + } + if transfer.From == transfer.To { + return nil, i18n.NewError(ctx, i18n.MsgCannotTransferToSelf) + } + + sender := am.NewTransfer(ns, typeName, poolName, transfer) + if waitConfirm { + err = sender.SendAndWait(ctx) + } else { + err = sender.Send(ctx) + } + return &transfer.TokenTransfer, err +} + +func (s *transferSender) resolveAndSend(ctx context.Context, waitConfirm bool) (err error) { + plugin, err := s.mgr.selectTokenPlugin(ctx, s.typeName) + if err != nil { + return err + } + pool, err := s.mgr.GetTokenPool(ctx, s.namespace, s.typeName, s.poolName) + if err != nil { + return err + } + s.transfer.PoolProtocolID = pool.ProtocolID + + var messageSender sysmessaging.MessageSender + if s.transfer.Message != nil { + if messageSender, err = s.buildTransferMessage(ctx, s.namespace, s.transfer.Message); err != nil { + return err + } + } + + switch { + case waitConfirm && messageSender != nil: + // prepare the message, send the transfer async, then send the message and wait + return messageSender. + BeforeSend(func(ctx context.Context) error { + s.transfer.MessageHash = s.transfer.Message.Hash + s.transfer.Message = nil + return s.Send(ctx) + }). + SendAndWait(ctx) + case waitConfirm: + // no message - just send the transfer and wait + return s.sendSync(ctx) + case messageSender != nil: + // send the message async and then move on to the transfer + if err := messageSender.Send(ctx); err != nil { + return err + } + s.transfer.MessageHash = s.transfer.Message.Hash + } + + tx := &fftypes.Transaction{ + ID: fftypes.NewUUID(), + Subject: fftypes.TransactionSubject{ + Namespace: s.namespace, + Type: fftypes.TransactionTypeTokenTransfer, + Signer: s.transfer.Key, + Reference: s.transfer.LocalID, + }, + Created: fftypes.Now(), + Status: fftypes.OpStatusPending, + } + tx.Hash = tx.Subject.Hash() + err = s.mgr.database.UpsertTransaction(ctx, tx, false /* should be new, or idempotent replay */) + if err != nil { + return err + } + + s.transfer.TX.ID = tx.ID + s.transfer.TX.Type = tx.Subject.Type + + op := fftypes.NewTXOperation( + plugin, + s.namespace, + tx.ID, + "", + fftypes.OpTypeTokenTransfer, + fftypes.OpStatusPending, + "") + addTokenTransferInputs(op, &s.transfer.TokenTransfer) + if err := s.mgr.database.UpsertOperation(ctx, op, false); err != nil { + return err + } + + if s.sendCallback != nil { + if err := s.sendCallback(ctx); err != nil { + return err + } + } + + switch s.transfer.Type { + case fftypes.TokenTransferTypeMint: + return plugin.MintTokens(ctx, op.ID, &s.transfer.TokenTransfer) + case fftypes.TokenTransferTypeTransfer: + return plugin.TransferTokens(ctx, op.ID, &s.transfer.TokenTransfer) + case fftypes.TokenTransferTypeBurn: + return plugin.BurnTokens(ctx, op.ID, &s.transfer.TokenTransfer) + default: + panic(fmt.Sprintf("unknown transfer type: %v", s.transfer.Type)) + } +} + +func (s *transferSender) sendSync(ctx context.Context) error { + out, err := s.mgr.syncasync.SendConfirmTokenTransfer(ctx, s.namespace, s.transfer.LocalID, s.Send) + if out != nil { + s.transfer.TokenTransfer = *out + } + return err +} + +func (s *transferSender) buildTransferMessage(ctx context.Context, ns string, in *fftypes.MessageInOut) (sysmessaging.MessageSender, error) { + allowedTypes := []fftypes.FFEnum{ + fftypes.MessageTypeTransferBroadcast, + fftypes.MessageTypeTransferPrivate, + } + if in.Header.Type == "" { + in.Header.Type = fftypes.MessageTypeTransferBroadcast + } + switch in.Header.Type { + case fftypes.MessageTypeTransferBroadcast: + return s.mgr.broadcast.NewBroadcast(ns, in), nil + case fftypes.MessageTypeTransferPrivate: + return s.mgr.messaging.NewMessage(ns, in), nil + default: + return nil, i18n.NewError(ctx, i18n.MsgInvalidMessageType, allowedTypes) + } +} diff --git a/internal/assets/token_transfer_test.go b/internal/assets/token_transfer_test.go new file mode 100644 index 0000000000..e91baf176e --- /dev/null +++ b/internal/assets/token_transfer_test.go @@ -0,0 +1,769 @@ +// Copyright © 2021 Kaleido, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in comdiliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or imdilied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package assets + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly/internal/syncasync" + "github.com/hyperledger/firefly/internal/sysmessaging" + "github.com/hyperledger/firefly/mocks/broadcastmocks" + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/datamocks" + "github.com/hyperledger/firefly/mocks/identitymanagermocks" + "github.com/hyperledger/firefly/mocks/privatemessagingmocks" + "github.com/hyperledger/firefly/mocks/syncasyncmocks" + "github.com/hyperledger/firefly/mocks/sysmessagingmocks" + "github.com/hyperledger/firefly/mocks/tokenmocks" + "github.com/hyperledger/firefly/pkg/database" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetTokenTransfers(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + pool := &fftypes.TokenPool{ + ID: fftypes.NewUUID(), + } + mdi := am.database.(*databasemocks.Plugin) + fb := database.TokenTransferQueryFactory.NewFilter(context.Background()) + f := fb.And() + mdi.On("GetTokenPool", context.Background(), "ns1", "test").Return(pool, nil) + mdi.On("GetTokenTransfers", context.Background(), f).Return([]*fftypes.TokenTransfer{}, nil, nil) + _, _, err := am.GetTokenTransfers(context.Background(), "ns1", "magic-tokens", "test", f) + assert.NoError(t, err) +} + +func TestGetTokenTransfersBadPool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdi := am.database.(*databasemocks.Plugin) + fb := database.TokenTransferQueryFactory.NewFilter(context.Background()) + f := fb.And() + mdi.On("GetTokenPool", context.Background(), "ns1", "test").Return(nil, fmt.Errorf("pop")) + _, _, err := am.GetTokenTransfers(context.Background(), "ns1", "magic-tokens", "test", f) + assert.EqualError(t, err, "pop") +} + +func TestGetTokenTransfersNoPool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mdi := am.database.(*databasemocks.Plugin) + fb := database.TokenTransferQueryFactory.NewFilter(context.Background()) + f := fb.And() + mdi.On("GetTokenPool", context.Background(), "ns1", "test").Return(nil, nil) + _, _, err := am.GetTokenTransfers(context.Background(), "ns1", "magic-tokens", "test", f) + assert.Regexp(t, "FF10109", err) +} + +func TestMintTokensSuccess(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mint := &fftypes.TokenTransferInput{} + mint.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.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(&fftypes.TokenPool{}, nil) + mti.On("MintTokens", context.Background(), mock.Anything, &mint.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + + _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) + assert.NoError(t, err) +} + +func TestMintTokensBadPlugin(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + + _, err := am.MintTokens(context.Background(), "", "", "", &fftypes.TokenTransferInput{}, false) + assert.Regexp(t, "FF10272", err) +} + +func TestMintTokensBadPool(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mint := &fftypes.TokenTransferInput{} + mint.Amount.Int().SetInt64(5) + + 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(nil, fmt.Errorf("pop")) + + _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) + assert.EqualError(t, err, "pop") +} + +func TestMintTokensIdentityFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mint := &fftypes.TokenTransferInput{} + mint.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(nil, fmt.Errorf("pop")) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + + _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) + assert.EqualError(t, err, "pop") +} + +func TestMintTokensFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mint := &fftypes.TokenTransferInput{} + mint.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.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(&fftypes.TokenPool{}, nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mti.On("MintTokens", context.Background(), mock.Anything, &mint.TokenTransfer).Return(fmt.Errorf("pop")) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + + _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) + assert.EqualError(t, err, "pop") +} + +func TestMintTokensOperationFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mint := &fftypes.TokenTransferInput{} + mint.Amount.Int().SetInt64(5) + + 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(&fftypes.TokenPool{}, nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(fmt.Errorf("pop")) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + + _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, false) + assert.EqualError(t, err, "pop") +} + +func TestMintTokensConfirm(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + mint := &fftypes.TokenTransferInput{} + mint.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mdm := am.data.(*datamocks.Manager) + msa := am.syncasync.(*syncasyncmocks.Bridge) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mti.On("MintTokens", context.Background(), mock.Anything, &mint.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + msa.On("SendConfirmTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + send := args[3].(syncasync.RequestSender) + send(context.Background()) + }). + Return(&fftypes.TokenTransfer{}, nil) + + _, err := am.MintTokens(context.Background(), "ns1", "magic-tokens", "pool1", mint, true) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + msa.AssertExpectations(t) + mti.AssertExpectations(t) +} + +func TestBurnTokensSuccess(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + burn := &fftypes.TokenTransferInput{} + burn.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.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(&fftypes.TokenPool{}, nil) + mti.On("BurnTokens", context.Background(), mock.Anything, &burn.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + + _, err := am.BurnTokens(context.Background(), "ns1", "magic-tokens", "pool1", burn, false) + assert.NoError(t, err) + + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mti.AssertExpectations(t) +} + +func TestBurnTokensIdentityFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + burn := &fftypes.TokenTransferInput{} + burn.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(nil, fmt.Errorf("pop")) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + + _, err := am.BurnTokens(context.Background(), "ns1", "magic-tokens", "pool1", burn, false) + assert.EqualError(t, err, "pop") +} + +func TestBurnTokensConfirm(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + burn := &fftypes.TokenTransferInput{} + burn.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mdm := am.data.(*datamocks.Manager) + msa := am.syncasync.(*syncasyncmocks.Bridge) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mti.On("BurnTokens", context.Background(), mock.Anything, &burn.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + msa.On("SendConfirmTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + send := args[3].(syncasync.RequestSender) + send(context.Background()) + }). + Return(&fftypes.TokenTransfer{}, nil) + + _, err := am.BurnTokens(context.Background(), "ns1", "magic-tokens", "pool1", burn, true) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + msa.AssertExpectations(t) + mti.AssertExpectations(t) +} + +func TestTransferTokensSuccess(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.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(&fftypes.TokenPool{}, nil) + mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) + assert.NoError(t, err) + + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mti.AssertExpectations(t) +} + +func TestTransferTokensIdentityFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(nil, fmt.Errorf("pop")) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) + assert.EqualError(t, err, "pop") +} + +func TestTransferTokensNoFromOrTo(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{} + + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) + assert.Regexp(t, "FF10280", err) + + mim.AssertExpectations(t) +} + +func TestTransferTokensInvalidType(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mdi.On("GetTokenPool", am.ctx, "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mdi.On("UpsertTransaction", am.ctx, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + mdi.On("UpsertOperation", am.ctx, mock.Anything, false).Return(nil) + + sender := &transferSender{ + mgr: am, + namespace: "ns1", + typeName: "magic-tokens", + poolName: "pool1", + transfer: transfer, + } + assert.PanicsWithValue(t, "unknown transfer type: ", func() { + sender.Send(am.ctx) + }) +} + +func TestTransferTokensTransactionFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + } + transfer.Amount.Int().SetInt64(5) + + 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(&fftypes.TokenPool{}, nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(fmt.Errorf("pop")) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) + assert.EqualError(t, err, "pop") + + mim.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestTransferTokensWithBroadcastMessage(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + hash := fftypes.NewRandB32() + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + Message: &fftypes.MessageInOut{ + Message: fftypes.Message{ + Hash: hash, + }, + InlineData: fftypes.InlineData{ + { + Value: []byte("test data"), + }, + }, + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mbm := am.broadcast.(*broadcastmocks.Manager) + mms := &sysmessagingmocks.MessageSender{} + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) + mms.On("Send", context.Background()).Return(nil) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) + assert.NoError(t, err) + assert.Equal(t, *hash, *transfer.MessageHash) + + mbm.AssertExpectations(t) + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mti.AssertExpectations(t) + mms.AssertExpectations(t) +} + +func TestTransferTokensWithBroadcastMessageFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + Message: &fftypes.MessageInOut{ + InlineData: fftypes.InlineData{ + { + Value: []byte("test data"), + }, + }, + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mbm := am.broadcast.(*broadcastmocks.Manager) + mms := &sysmessagingmocks.MessageSender{} + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) + mms.On("Send", context.Background()).Return(fmt.Errorf("pop")) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) + assert.EqualError(t, err, "pop") + + mbm.AssertExpectations(t) + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mms.AssertExpectations(t) +} + +func TestTransferTokensWithPrivateMessage(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + hash := fftypes.NewRandB32() + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + Message: &fftypes.MessageInOut{ + Message: fftypes.Message{ + Header: fftypes.MessageHeader{ + Type: fftypes.MessageTypeTransferPrivate, + }, + Hash: hash, + }, + InlineData: fftypes.InlineData{ + { + Value: []byte("test data"), + }, + }, + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mpm := am.messaging.(*privatemessagingmocks.Manager) + mms := &sysmessagingmocks.MessageSender{} + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + mpm.On("NewMessage", "ns1", transfer.Message).Return(mms) + mms.On("Send", context.Background()).Return(nil) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) + assert.NoError(t, err) + assert.Equal(t, *hash, *transfer.MessageHash) + + mpm.AssertExpectations(t) + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mti.AssertExpectations(t) + mms.AssertExpectations(t) +} + +func TestTransferTokensWithInvalidMessage(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + Message: &fftypes.MessageInOut{ + Message: fftypes.Message{ + Header: fftypes.MessageHeader{ + Type: fftypes.MessageTypeDefinition, + }, + }, + InlineData: fftypes.InlineData{ + { + Value: []byte("test data"), + }, + }, + }, + } + transfer.Amount.Int().SetInt64(5) + + 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(&fftypes.TokenPool{}, nil) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, false) + assert.Regexp(t, "FF10287", err) + + mim.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestTransferTokensConfirm(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mdm := am.data.(*datamocks.Manager) + msa := am.syncasync.(*syncasyncmocks.Bridge) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + msa.On("SendConfirmTokenTransfer", context.Background(), "ns1", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + send := args[3].(syncasync.RequestSender) + send(context.Background()) + }). + Return(&fftypes.TokenTransfer{}, nil) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, true) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mdm.AssertExpectations(t) + msa.AssertExpectations(t) + mti.AssertExpectations(t) +} + +func TestTransferTokensBeforeSendCallback(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + Type: fftypes.TokenTransferTypeTransfer, + From: "A", + To: "B", + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + + sender := am.NewTransfer("ns1", "magic-tokens", "pool1", transfer) + + called := false + sender.BeforeSend(func(ctx context.Context) error { + called = true + return nil + }) + + err := sender.Send(context.Background()) + assert.NoError(t, err) + assert.True(t, called) + + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mti.AssertExpectations(t) +} + +func TestTransferTokensBeforeSendCallbackFail(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + Type: fftypes.TokenTransferTypeTransfer, + From: "A", + To: "B", + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + + sender := am.NewTransfer("ns1", "magic-tokens", "pool1", transfer) + + called := false + sender.BeforeSend(func(ctx context.Context) error { + called = true + return fmt.Errorf("pop") + }) + + err := sender.Send(context.Background()) + assert.EqualError(t, err, "pop") + assert.True(t, called) + + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mti.AssertExpectations(t) +} + +func TestTransferTokensWithBroadcastMessageConfirm(t *testing.T) { + am, cancel := newTestAssets(t) + defer cancel() + + hash := fftypes.NewRandB32() + transfer := &fftypes.TokenTransferInput{ + TokenTransfer: fftypes.TokenTransfer{ + From: "A", + To: "B", + }, + Message: &fftypes.MessageInOut{ + Message: fftypes.Message{ + Hash: hash, + }, + InlineData: fftypes.InlineData{ + { + Value: []byte("test data"), + }, + }, + }, + } + transfer.Amount.Int().SetInt64(5) + + mdi := am.database.(*databasemocks.Plugin) + mti := am.tokens["magic-tokens"].(*tokenmocks.Plugin) + mim := am.identity.(*identitymanagermocks.Manager) + mbm := am.broadcast.(*broadcastmocks.Manager) + mms := &sysmessagingmocks.MessageSender{} + mim.On("GetLocalOrganization", context.Background()).Return(&fftypes.Organization{Identity: "0x12345"}, nil) + mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(&fftypes.TokenPool{}, nil) + mti.On("TransferTokens", context.Background(), mock.Anything, &transfer.TokenTransfer).Return(nil) + mdi.On("UpsertOperation", context.Background(), mock.Anything, false).Return(nil) + mdi.On("UpsertTransaction", context.Background(), mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Subject.Type == fftypes.TransactionTypeTokenTransfer + }), false).Return(nil) + mbm.On("NewBroadcast", "ns1", transfer.Message).Return(mms) + mms.On("BeforeSend", mock.Anything). + Run(func(args mock.Arguments) { + cb := args[0].(sysmessaging.BeforeSendCallback) + cb(context.Background()) + }). + Return(mms) + mms.On("SendAndWait", context.Background()).Return(nil) + + _, err := am.TransferTokens(context.Background(), "ns1", "magic-tokens", "pool1", transfer, true) + assert.NoError(t, err) + assert.Nil(t, transfer.Message) + assert.Equal(t, *hash, *transfer.MessageHash) + + mbm.AssertExpectations(t) + mim.AssertExpectations(t) + mdi.AssertExpectations(t) + mti.AssertExpectations(t) + mms.AssertExpectations(t) +}