Skip to content

Commit

Permalink
Merge pull request #29 from alex-semenyuk/query_invoke
Browse files Browse the repository at this point in the history
QueryInvoke implementation
  • Loading branch information
denisandreenko committed Jan 6, 2024
2 parents 3491831 + d56e673 commit f060841
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 11 deletions.
67 changes: 56 additions & 11 deletions internal/tezos/exec_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
164 changes: 164 additions & 0 deletions internal/tezos/exec_query_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 12 additions & 0 deletions internal/tezos/prepare_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit f060841

Please sign in to comment.