diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 989140d560..cb19846717 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -5252,6 +5252,7 @@ paths: - token_create_pool - token_announce_pool - token_transfer + - contract_invoke type: string updated: {} type: object @@ -5301,6 +5302,7 @@ paths: - batch_pin - token_pool - token_transfer + - contract_invoke type: string type: object description: Success @@ -6010,6 +6012,7 @@ paths: - token_create_pool - token_announce_pool - token_transfer + - contract_invoke type: string updated: {} type: object @@ -6075,6 +6078,7 @@ paths: - token_create_pool - token_announce_pool - token_transfer + - contract_invoke type: string updated: {} type: object @@ -9976,6 +9980,7 @@ paths: - batch_pin - token_pool - token_transfer + - contract_invoke type: string type: object description: Success @@ -10083,6 +10088,7 @@ paths: - batch_pin - token_pool - token_transfer + - contract_invoke type: string type: object description: Success @@ -10132,6 +10138,7 @@ paths: - batch_pin - token_pool - token_transfer + - contract_invoke type: string type: object type: array diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index 82d1c22af3..120b4f62b4 100644 --- a/internal/contracts/manager.go +++ b/internal/contracts/manager.go @@ -149,10 +149,37 @@ func (cm *contractManager) InvokeContract(ctx context.Context, ns string, req *f return nil, err } - if req.Method, err = cm.resolveInvokeContractRequest(ctx, ns, req); err != nil { - return nil, err - } - if err := cm.validateInvokeContractRequest(ctx, req); err != nil { + var op *fftypes.Operation + err = cm.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { + if req.Method, err = cm.resolveInvokeContractRequest(ctx, ns, req); err != nil { + return err + } + if err := cm.validateInvokeContractRequest(ctx, req); err != nil { + return err + } + + tx := &fftypes.Transaction{ + ID: fftypes.NewUUID(), + Namespace: ns, + Type: fftypes.TransactionTypeContractInvoke, + Created: fftypes.Now(), + Status: fftypes.OpStatusPending, + } + if err := cm.database.UpsertTransaction(ctx, tx); err != nil { + return err + } + + op = fftypes.NewTXOperation( + cm.blockchain, + ns, + tx.ID, + "", + fftypes.OpTypeContractInvoke, + fftypes.OpStatusPending) + op.Input = req.Input + return cm.database.InsertOperation(ctx, op) + }) + if err != nil { return nil, err } diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index 683fb834ad..7cd2fc3772 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -40,6 +40,8 @@ func newTestContractManager() *contractManager { mim := &identitymanagermocks.Manager{} mbi := &blockchainmocks.Plugin{} + mbi.On("Name").Return("mockblockchain").Maybe() + rag := mdb.On("RunAsGroup", mock.Anything, mock.Anything).Maybe() rag.RunFn = func(a mock.Arguments) { rag.ReturnArguments = mock.Arguments{ @@ -1070,6 +1072,7 @@ func TestInvokeContract(t *testing.T) { cm := newTestContractManager() mbi := cm.blockchain.(*blockchainmocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) + mdi := cm.database.(*databasemocks.Plugin) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeInvoke, @@ -1085,6 +1088,12 @@ func TestInvokeContract(t *testing.T) { } mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) + mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke + })).Return(nil) + mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Namespace == "ns1" && op.Type == fftypes.OpTypeContractInvoke && op.Plugin == "mockblockchain" + })).Return(nil) mbi.On("InvokeContract", mock.Anything, mock.AnythingOfType("*fftypes.UUID"), "key-resolved", req.Location, req.Method, req.Input).Return(struct{}{}, nil) _, err := cm.InvokeContract(context.Background(), "ns1", req) @@ -1130,9 +1139,36 @@ func TestInvokeContractFailResolve(t *testing.T) { assert.Regexp(t, "FF10313", err) } +func TestInvokeContractTXFail(t *testing.T) { + cm := newTestContractManager() + mim := cm.identity.(*identitymanagermocks.Manager) + mdi := cm.database.(*databasemocks.Plugin) + + req := &fftypes.ContractCallRequest{ + Type: fftypes.CallTypeInvoke, + Interface: fftypes.NewUUID(), + Ledger: fftypes.JSONAnyPtr(""), + Location: fftypes.JSONAnyPtr(""), + Method: &fftypes.FFIMethod{ + Name: "doStuff", + ID: fftypes.NewUUID(), + Params: fftypes.FFIParams{}, + Returns: fftypes.FFIParams{}, + }, + } + + mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) + mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke + })).Return(fmt.Errorf("pop")) + + _, err := cm.InvokeContract(context.Background(), "ns1", req) + + assert.EqualError(t, err, "pop") +} + func TestInvokeContractNoMethodSignature(t *testing.T) { cm := newTestContractManager() - mbi := cm.blockchain.(*blockchainmocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) req := &fftypes.ContractCallRequest{ @@ -1145,7 +1181,6 @@ func TestInvokeContractNoMethodSignature(t *testing.T) { } mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) - mbi.On("InvokeContract", mock.Anything, mock.AnythingOfType("*fftypes.UUID"), "key-resolved", mock.Anything, mock.AnythingOfType("*fftypes.FFIMethod"), mock.Anything).Return(struct{}{}, nil) _, err := cm.InvokeContract(context.Background(), "ns1", req) @@ -1220,6 +1255,7 @@ func TestQueryContract(t *testing.T) { cm := newTestContractManager() mbi := cm.blockchain.(*blockchainmocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) + mdi := cm.database.(*databasemocks.Plugin) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeQuery, @@ -1235,6 +1271,12 @@ func TestQueryContract(t *testing.T) { } mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) + mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke + })).Return(nil) + mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Namespace == "ns1" && op.Type == fftypes.OpTypeContractInvoke && op.Plugin == "mockblockchain" + })).Return(nil) mbi.On("QueryContract", mock.Anything, req.Location, req.Method, req.Input).Return(struct{}{}, nil) _, err := cm.InvokeContract(context.Background(), "ns1", req) @@ -1245,6 +1287,7 @@ func TestQueryContract(t *testing.T) { func TestCallContractInvalidType(t *testing.T) { cm := newTestContractManager() mim := cm.identity.(*identitymanagermocks.Manager) + mdi := cm.database.(*databasemocks.Plugin) req := &fftypes.ContractCallRequest{ Interface: fftypes.NewUUID(), @@ -1259,6 +1302,12 @@ func TestCallContractInvalidType(t *testing.T) { } mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) + mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke + })).Return(nil) + mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Namespace == "ns1" && op.Type == fftypes.OpTypeContractInvoke && op.Plugin == "mockblockchain" + })).Return(nil) assert.PanicsWithValue(t, "unknown call type: ", func() { cm.InvokeContract(context.Background(), "ns1", req) @@ -1406,6 +1455,7 @@ func TestInvokeContractAPI(t *testing.T) { mdb := cm.database.(*databasemocks.Plugin) mim := cm.identity.(*identitymanagermocks.Manager) mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) req := &fftypes.ContractCallRequest{ Type: fftypes.CallTypeInvoke, @@ -1427,6 +1477,12 @@ func TestInvokeContractAPI(t *testing.T) { mim.On("ResolveSigningKey", mock.Anything, "").Return("key-resolved", nil) mdb.On("GetContractAPIByName", mock.Anything, "ns1", "banana").Return(api, nil) mdb.On("GetFFIMethod", mock.Anything, "ns1", mock.Anything, mock.Anything).Return(&fftypes.FFIMethod{Name: "peel"}, nil) + mdi.On("UpsertTransaction", mock.Anything, mock.MatchedBy(func(tx *fftypes.Transaction) bool { + return tx.Namespace == "ns1" && tx.Type == fftypes.TransactionTypeContractInvoke + })).Return(nil) + mdi.On("InsertOperation", mock.Anything, mock.MatchedBy(func(op *fftypes.Operation) bool { + return op.Namespace == "ns1" && op.Type == fftypes.OpTypeContractInvoke && op.Plugin == "mockblockchain" + })).Return(nil) mbi.On("InvokeContract", mock.Anything, mock.AnythingOfType("*fftypes.UUID"), "key-resolved", req.Location, mock.AnythingOfType("*fftypes.FFIMethod"), req.Input).Return(struct{}{}, nil) _, err := cm.InvokeContractAPI(context.Background(), "ns1", "banana", "peel", req) diff --git a/pkg/fftypes/operation.go b/pkg/fftypes/operation.go index e8537c58a1..472fdc75b7 100644 --- a/pkg/fftypes/operation.go +++ b/pkg/fftypes/operation.go @@ -1,4 +1,4 @@ -// Copyright © 2021 Kaleido, Inc. +// Copyright © 2022 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -36,6 +36,8 @@ var ( OpTypeTokenAnnouncePool OpType = ffEnum("optype", "token_announce_pool") // OpTypeTokenTransfer is a token transfer OpTypeTokenTransfer OpType = ffEnum("optype", "token_transfer") + // OpTypeContractInvoke is a smart contract invoke + OpTypeContractInvoke OpType = ffEnum("optype", "contract_invoke") ) // OpStatus is the current status of an operation diff --git a/pkg/fftypes/transaction.go b/pkg/fftypes/transaction.go index 7032db192b..a9d539e9e8 100644 --- a/pkg/fftypes/transaction.go +++ b/pkg/fftypes/transaction.go @@ -27,6 +27,8 @@ var ( TransactionTypeTokenPool TransactionType = ffEnum("txtype", "token_pool") // TransactionTypeTokenTransfer represents a token transfer TransactionTypeTokenTransfer TransactionType = ffEnum("txtype", "token_transfer") + // TransactionTypeContractInvoke is a smart contract invoke + TransactionTypeContractInvoke OpType = ffEnum("txtype", "contract_invoke") ) // TransactionRef refers to a transaction, in other types