diff --git a/internal/tezos/exec_query.go b/internal/tezos/exec_query.go index c2a8803..2338ba0 100644 --- a/internal/tezos/exec_query.go +++ b/internal/tezos/exec_query.go @@ -2,27 +2,72 @@ package tezos import ( "context" + "encoding/json" + "errors" + "strings" - "blockwatch.cc/tzgo/codec" + "blockwatch.cc/tzgo/micheline" "blockwatch.cc/tzgo/rpc" + "blockwatch.cc/tzgo/tezos" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-tezosconnect/internal/msgs" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) // QueryInvoke executes a method on a blockchain smart contract, which might execute Smart Contract code, but does not affect the blockchain state. -func (c *tezosConnector) QueryInvoke(_ context.Context, req *ffcapi.QueryInvokeRequest) (*ffcapi.QueryInvokeResponse, ffcapi.ErrorReason, error) { - // TODO: to implement - return nil, "", nil +func (c *tezosConnector) QueryInvoke(ctx context.Context, req *ffcapi.QueryInvokeRequest) (*ffcapi.QueryInvokeResponse, ffcapi.ErrorReason, error) { + if req == nil { + return nil, ffcapi.ErrorReasonInvalidInputs, errors.New("request is not defined") + } + + params, err := c.prepareInputParams(ctx, &req.TransactionInput) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + resp, err := c.runView(ctx, params.Entrypoint, req.From, req.To, params.Value) + if err != nil { + return nil, ffcapi.ErrorReasonTransactionReverted, err + } + + outputs, _ := json.Marshal(resp) + if val, ok := resp.(string); ok { + if values := strings.Split(val, ","); len(values) > 1 { + outputs, _ = json.Marshal(values) + } + } + return &ffcapi.QueryInvokeResponse{ + Outputs: fftypes.JSONAnyPtrBytes(outputs), + }, "", nil } -func (c *tezosConnector) callTransaction(ctx context.Context, op *codec.Op, opts *rpc.CallOptions) (*rpc.Receipt, ffcapi.ErrorReason, error) { - sim, err := c.client.Simulate(ctx, op, opts) +func (c *tezosConnector) runView(ctx context.Context, entrypoint, addrFrom, addrTo string, args micheline.Prim) (interface{}, error) { + toAddress, err := tezos.ParseAddress(addrTo) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgInvalidToAddress, addrTo, err) + } + + fromAddress, err := tezos.ParseAddress(addrFrom) if err != nil { - return nil, mapError(callRPCMethods, err), err + return nil, i18n.NewError(ctx, msgs.MsgInvalidFromAddress, addrFrom, err) } - // fail with Tezos error when simulation failed - if !sim.IsSuccess() { - return nil, mapError(callRPCMethods, sim.Error()), sim.Error() + + req := rpc.RunViewRequest{ + Contract: toAddress, + View: entrypoint, + Input: args, + Source: fromAddress, + Payer: fromAddress, + UnlimitedGas: true, + Mode: "Readable", + } + + var res rpc.RunViewResponse + err = c.client.RunView(ctx, rpc.Head, &req, &res) + if err != nil { + return nil, err } - return sim, "", nil + return res.Data.Value(res.Data.OpCode), nil } diff --git a/internal/tezos/exec_query_test.go b/internal/tezos/exec_query_test.go new file mode 100644 index 0000000..9b413a5 --- /dev/null +++ b/internal/tezos/exec_query_test.go @@ -0,0 +1,164 @@ +package tezos + +import ( + "math/big" + "os" + "testing" + + "blockwatch.cc/tzgo/micheline" + "blockwatch.cc/tzgo/rpc" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestQueryInvokeSuccess(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + req := &ffcapi.QueryInvokeRequest{ + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr("\"simple_view\""), + }, + } + res := rpc.RunViewResponse{ + Data: micheline.Prim{ + Type: micheline.PrimString, + String: "3", + }, + } + mRPC.On("RunView", ctx, mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + arg := args.Get(3).(*rpc.RunViewResponse) + *arg = res + }) + + resp, reason, err := c.QueryInvoke(ctx, req) + + assert.NotNil(t, resp) + assert.Equal(t, resp.Outputs.String(), "\"3\"") + assert.Empty(t, reason) + assert.NoError(t, err) +} + +func TestQueryInvokeSuccessArray(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + req := &ffcapi.QueryInvokeRequest{ + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr("\"simple_view\""), + }, + } + res := rpc.RunViewResponse{ + Data: micheline.Prim{ + Type: micheline.PrimSequence, + OpCode: micheline.D_PAIR, + Args: []micheline.Prim{ + {Type: micheline.PrimString, String: "str"}, + {Type: micheline.PrimInt, Int: big.NewInt(1)}, + }, + }, + } + mRPC.On("RunView", ctx, mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + arg := args.Get(3).(*rpc.RunViewResponse) + *arg = res + }) + + resp, reason, err := c.QueryInvoke(ctx, req) + + assert.NotNil(t, resp) + assert.Equal(t, resp.Outputs.String(), "[\"str\",\"1\"]") + assert.Empty(t, reason) + assert.NoError(t, err) +} + +func TestQueryInvokeRunViewError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + req := &ffcapi.QueryInvokeRequest{ + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr("\"simple_view\""), + }, + } + mRPC.On("RunView", ctx, mock.Anything, mock.Anything, mock.Anything).Return(assert.AnError) + + resp, reason, err := c.QueryInvoke(ctx, req) + + assert.Nil(t, resp) + assert.Equal(t, reason, ffcapi.ErrorReasonTransactionReverted) + assert.Error(t, err) +} + +func TestQueryInvokeWrongParamsError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + os.Setenv("ENV", "test") + req := &ffcapi.QueryInvokeRequest{ + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr("\"simple_view\""), + Params: []*fftypes.JSONAny{ + fftypes.JSONAnyPtr("wrong"), + }, + }, + } + + resp, reason, err := c.QueryInvoke(ctx, req) + + assert.Nil(t, resp) + assert.Equal(t, reason, ffcapi.ErrorReasonInvalidInputs) + assert.Error(t, err) +} + +func TestQueryInvokeParseAddressToError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + req := &ffcapi.QueryInvokeRequest{ + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr("\"simple_view\""), + TransactionHeaders: ffcapi.TransactionHeaders{ + To: "t......", + }, + }, + } + + resp, reason, err := c.QueryInvoke(ctx, req) + + assert.Nil(t, resp) + assert.Equal(t, reason, ffcapi.ErrorReasonTransactionReverted) + assert.Error(t, err) +} + +func TestQueryInvokeParseAddressFromError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + req := &ffcapi.QueryInvokeRequest{ + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr("\"simple_view\""), + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "t......", + }, + }, + } + + resp, reason, err := c.QueryInvoke(ctx, req) + + assert.Nil(t, resp) + assert.Equal(t, reason, ffcapi.ErrorReasonTransactionReverted) + assert.Error(t, err) +} + +func TestQueryInvokeRequestNotDefinedError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + resp, reason, err := c.QueryInvoke(ctx, nil) + + assert.Nil(t, resp) + assert.Equal(t, reason, ffcapi.ErrorReasonInvalidInputs) + assert.Error(t, err) +} diff --git a/internal/tezos/prepare_transaction.go b/internal/tezos/prepare_transaction.go index 2566c40..d72fd49 100644 --- a/internal/tezos/prepare_transaction.go +++ b/internal/tezos/prepare_transaction.go @@ -81,6 +81,18 @@ func (c *tezosConnector) estimateAndAssignTxCost(ctx context.Context, op *codec. return "", nil } +func (c *tezosConnector) callTransaction(ctx context.Context, op *codec.Op, opts *rpc.CallOptions) (*rpc.Receipt, ffcapi.ErrorReason, error) { + sim, err := c.client.Simulate(ctx, op, opts) + if err != nil { + return nil, mapError(callRPCMethods, err), err + } + // fail with Tezos error when simulation failed + if !sim.IsSuccess() { + return nil, mapError(callRPCMethods, sim.Error()), sim.Error() + } + return sim, "", nil +} + func (c *tezosConnector) prepareInputParams(ctx context.Context, req *ffcapi.TransactionInput) (micheline.Parameters, error) { var tezosParams micheline.Parameters