diff --git a/internal/tezos/blocklistener_test.go b/internal/tezos/blocklistener_test.go new file mode 100644 index 0000000..59c80ba --- /dev/null +++ b/internal/tezos/blocklistener_test.go @@ -0,0 +1,47 @@ +package tezos + +import ( + "errors" + "testing" + + "blockwatch.cc/tzgo/rpc" + "blockwatch.cc/tzgo/tezos" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestBlockListenerStartGettingHighestBlockRetry(t *testing.T) { + _, c, mRPC, done := newTestConnector(t) + bl := c.blockListener + + mRPC.On("GetHeadBlock", mock.Anything).Return(nil, errors.New("err")).Once() + mRPC.On("GetHeadBlock", mock.Anything).Return( + &rpc.Block{ + Hash: tezos.MustParseBlockHash("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg"), + Header: rpc.BlockHeader{ + Predecessor: tezos.MustParseBlockHash("BLc1BjmZ7WevMoMoj8jxh4k2wLoRqoMUxjrQuDmKzAsApfRRjFL"), + Level: 12345, + }, + }, nil) + + assert.Equal(t, int64(12345), bl.getHighestBlock(bl.ctx)) + done() // Stop immediately in this case, while we're in the polling interval + + <-bl.listenLoopDone + + mRPC.AssertExpectations(t) +} + +func TestBlockListenerStartGettingHighestBlockFailBeforeStop(t *testing.T) { + _, c, mRPC, done := newTestConnector(t) + done() // Stop before we start + bl := c.blockListener + + mRPC.On("GetHeadBlock", mock.Anything).Return(nil, errors.New("err")).Maybe() + + assert.Equal(t, int64(-1), bl.getHighestBlock(bl.ctx)) + + <-bl.listenLoopDone + + mRPC.AssertExpectations(t) +} diff --git a/internal/tezos/get_address_balance_test.go b/internal/tezos/get_address_balance_test.go index 8771076..21ab015 100644 --- a/internal/tezos/get_address_balance_test.go +++ b/internal/tezos/get_address_balance_test.go @@ -1,12 +1,34 @@ package tezos import ( + "errors" "testing" + "blockwatch.cc/tzgo/rpc" + "blockwatch.cc/tzgo/tezos" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) +func TestGetAddressBalanceOK(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetHeadBlock", mock.Anything).Return(&rpc.Block{ + Hash: tezos.MustParseBlockHash("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg"), + }, nil) + mRPC.On("GetContractBalance", mock.Anything, mock.Anything, mock.Anything). + Return(tezos.NewZ(999), nil) + + req := ffcapi.AddressBalanceRequest{ + Address: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + } + res, _, err := c.AddressBalance(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, int64(999), res.Balance.Int64()) +} + func TestGetAddressWrongAddress(t *testing.T) { ctx, c, _, done := newTestConnector(t) defer done() @@ -14,8 +36,37 @@ func TestGetAddressWrongAddress(t *testing.T) { req := &ffcapi.AddressBalanceRequest{ Address: "wrong", } - _, reason, err := c.AddressBalance(ctx, req) assert.Error(t, err) assert.Equal(t, reason, ffcapi.ErrorReasonInvalidInputs) } + +func TestGetAddressBalanceGetHeadBlockErr(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetHeadBlock", mock.Anything).Return(nil, errors.New("err")) + + req := ffcapi.AddressBalanceRequest{ + Address: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + } + _, _, err := c.AddressBalance(ctx, &req) + assert.Error(t, err) +} + +func TestGetAddressBalanceGetContractBalanceErr(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetHeadBlock", mock.Anything).Return(&rpc.Block{ + Hash: tezos.MustParseBlockHash("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg"), + }, nil) + mRPC.On("GetContractBalance", mock.Anything, mock.Anything, mock.Anything). + Return(tezos.Zero, errors.New("err")) + + req := ffcapi.AddressBalanceRequest{ + Address: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + } + _, _, err := c.AddressBalance(ctx, &req) + assert.Error(t, err) +} diff --git a/internal/tezos/get_block_info_test.go b/internal/tezos/get_block_info_test.go new file mode 100644 index 0000000..5cb0beb --- /dev/null +++ b/internal/tezos/get_block_info_test.go @@ -0,0 +1,196 @@ +package tezos + +import ( + "errors" + "testing" + + "blockwatch.cc/tzgo/rpc" + "blockwatch.cc/tzgo/tezos" + "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 TestGetBlockInfoByNumberOK(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlock", mock.Anything, mock.MatchedBy( + func(blockNumber *fftypes.FFBigInt) bool { + return blockNumber.String() == "12345" + })). + Return(&rpc.Block{ + Hash: tezos.MustParseBlockHash("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg"), + Header: rpc.BlockHeader{ + Predecessor: tezos.MustParseBlockHash("BLc1BjmZ7WevMoMoj8jxh4k2wLoRqoMUxjrQuDmKzAsApfRRjFL"), + Level: 12345, + }, + Operations: [][]*rpc.Operation{ + {}, // consensus operations + {}, // voting operations + {}, // anonymous operations + { + { + Hash: tezos.MustParseOpHash("op13B8GtoK1UAx8p67L8y4huPqjay9yzRrpSFLTiY9kjJsrF5uV"), + }, + }, // manager operations + }, + }, nil). + Twice() // two cache misses and a hit + + req := &ffcapi.BlockInfoByNumberRequest{ + BlockNumber: fftypes.NewFFBigInt(12345), + } + res, reason, err := c.BlockInfoByNumber(ctx, req) + assert.NoError(t, err) + assert.Empty(t, reason) + + assert.Equal(t, "BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg", res.BlockHash) + assert.Equal(t, "BLc1BjmZ7WevMoMoj8jxh4k2wLoRqoMUxjrQuDmKzAsApfRRjFL", res.ParentHash) + assert.Equal(t, int64(12345), res.BlockNumber.Int64()) + + res, reason, err = c.BlockInfoByNumber(ctx, req) // cached + assert.NoError(t, err) + assert.Equal(t, "BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg", res.BlockHash) + + req.ExpectedParentHash = "BMWDjzorc6GFb2DnengeB2TRikAENukebRwubnu6ghfZceicmig" + res, reason, err = c.BlockInfoByNumber(ctx, req) // cache miss + assert.NoError(t, err) + assert.Equal(t, "BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg", res.BlockHash) +} + +func TestGetBlockInfoByNumberBlockNotFoundError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlock", mock.Anything, mock.Anything). + Return(nil, errors.New("status 404")) + + req := &ffcapi.BlockInfoByNumberRequest{ + BlockNumber: fftypes.NewFFBigInt(1), + } + res, reason, err := c.BlockInfoByNumber(ctx, req) + assert.Regexp(t, "FF23011", err) + assert.Equal(t, ffcapi.ErrorReasonNotFound, reason) + assert.Nil(t, res) +} + +func TestGetBlockInfoByNumberNotFound(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlock", mock.Anything, mock.Anything). + Return(nil, nil) + + req := &ffcapi.BlockInfoByNumberRequest{ + BlockNumber: fftypes.NewFFBigInt(12345), + } + res, reason, err := c.BlockInfoByNumber(ctx, req) + assert.Regexp(t, "FF23011", err) + assert.Equal(t, ffcapi.ErrorReasonNotFound, reason) + assert.Nil(t, res) + +} + +func TestGetBlockInfoByNumberFail(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlock", mock.Anything, mock.Anything). + Return(nil, errors.New("err")) + + req := &ffcapi.BlockInfoByNumberRequest{ + BlockNumber: fftypes.NewFFBigInt(1), + } + res, reason, err := c.BlockInfoByNumber(ctx, req) + assert.Error(t, err) + assert.Empty(t, reason) + assert.Nil(t, res) +} + +func TestGetBlockInfoByHashOK(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlock", mock.Anything, mock.Anything). + Return(&rpc.Block{ + Hash: tezos.MustParseBlockHash("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg"), + Header: rpc.BlockHeader{ + Predecessor: tezos.MustParseBlockHash("BLc1BjmZ7WevMoMoj8jxh4k2wLoRqoMUxjrQuDmKzAsApfRRjFL"), + Level: 12345, + }, + Operations: [][]*rpc.Operation{ + {}, // consensus operations + {}, // voting operations + {}, // anonymous operations + { + { + Hash: tezos.MustParseOpHash("op13B8GtoK1UAx8p67L8y4huPqjay9yzRrpSFLTiY9kjJsrF5uV"), + }, + }, // manager operations + }, + }, nil). + Once() + + req := &ffcapi.BlockInfoByHashRequest{ + BlockHash: "BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg", + } + res, reason, err := c.BlockInfoByHash(ctx, req) + assert.NoError(t, err) + assert.Empty(t, reason) + + assert.Equal(t, "BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg", res.BlockHash) + assert.Equal(t, "BLc1BjmZ7WevMoMoj8jxh4k2wLoRqoMUxjrQuDmKzAsApfRRjFL", res.ParentHash) + assert.Equal(t, int64(12345), res.BlockNumber.Int64()) + + res, reason, err = c.BlockInfoByHash(ctx, req) // cached + assert.NoError(t, err) + assert.Empty(t, reason) + assert.Equal(t, "BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg", res.BlockHash) +} + +func TestGetBlockInfoByHashParseBlockHashFail(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + req := &ffcapi.BlockInfoByHashRequest{ + BlockHash: "wrong", + } + res, reason, err := c.BlockInfoByHash(ctx, req) + assert.Error(t, err) + assert.Empty(t, reason) + assert.Nil(t, res) +} + +func TestGetBlockInfoByHashNotFound(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlock", mock.Anything, mock.Anything). + Return(nil, nil) + + req := &ffcapi.BlockInfoByHashRequest{ + BlockHash: "BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg", + } + res, reason, err := c.BlockInfoByHash(ctx, req) + assert.Regexp(t, "FF23011", err) + assert.Equal(t, ffcapi.ErrorReasonNotFound, reason) + assert.Nil(t, res) +} + +func TestGetBlockInfoByHashFail(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlock", mock.Anything, mock.Anything). + Return(nil, errors.New("err")) + + req := &ffcapi.BlockInfoByHashRequest{ + BlockHash: "BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg", + } + res, reason, err := c.BlockInfoByHash(ctx, req) + assert.Error(t, err) + assert.Empty(t, reason) + assert.Nil(t, res) +} diff --git a/internal/tezos/get_next_nonce_test.go b/internal/tezos/get_next_nonce_test.go new file mode 100644 index 0000000..470dad5 --- /dev/null +++ b/internal/tezos/get_next_nonce_test.go @@ -0,0 +1,45 @@ +package tezos + +import ( + "errors" + "testing" + + "blockwatch.cc/tzgo/rpc" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetNextNonceOK(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetContractExt", mock.Anything, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + }, nil) + + req := &ffcapi.NextNonceForSignerRequest{ + Signer: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + } + res, reason, err := c.NextNonceForSigner(ctx, req) + assert.NoError(t, err) + assert.Empty(t, reason) + assert.Equal(t, int64(11), res.Nonce.Int64()) +} + +func TestGetNextNonceFail(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetContractExt", mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("err")) + + req := &ffcapi.NextNonceForSignerRequest{ + Signer: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + } + res, reason, err := c.NextNonceForSigner(ctx, req) + assert.Error(t, err) + assert.Empty(t, reason) + assert.Nil(t, res) +} diff --git a/internal/tezos/get_receipt.go b/internal/tezos/get_receipt.go index abf3f8a..92c0974 100644 --- a/internal/tezos/get_receipt.go +++ b/internal/tezos/get_receipt.go @@ -31,8 +31,9 @@ type receiptExtraInfo struct { // TransactionReceipt queries to see if a receipt is available for a given transaction hash func (c *tezosConnector) TransactionReceipt(ctx context.Context, req *ffcapi.TransactionReceiptRequest) (*ffcapi.TransactionReceiptResponse, ffcapi.ErrorReason, error) { // ensure block observer is running - mon := c.client.BlockObserver - mon.Listen(c.client) + rpcClient := c.client.(*rpc.Client) + mon := rpcClient.BlockObserver + mon.Listen(rpcClient) // wait for confirmations res := rpc.NewResult(tezos.MustParseOpHash(req.TransactionHash)) //.WithTTL(op.TTL).WithConfirmations(opts.Confirmations) diff --git a/internal/tezos/new_block_listener_test.go b/internal/tezos/new_block_listener_test.go new file mode 100644 index 0000000..f99b7c6 --- /dev/null +++ b/internal/tezos/new_block_listener_test.go @@ -0,0 +1,36 @@ +package tezos + +import ( + "testing" + + "blockwatch.cc/tzgo/rpc" + "blockwatch.cc/tzgo/tezos" + "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 TestNewBlockListenerOK(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + c.blockListener.blockPollingInterval = 1 + mRPC.On("GetHeadBlock", mock.Anything).Return( + &rpc.Block{ + Hash: tezos.MustParseBlockHash("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg"), + Header: rpc.BlockHeader{ + Predecessor: tezos.MustParseBlockHash("BLc1BjmZ7WevMoMoj8jxh4k2wLoRqoMUxjrQuDmKzAsApfRRjFL"), + Level: 12345, + }, + }, nil).Maybe() + + req := &ffcapi.NewBlockListenerRequest{ + ID: fftypes.NewUUID(), + ListenerContext: ctx, + BlockListener: make(chan<- *ffcapi.BlockHashEvent), + } + res, _, err := c.NewBlockListener(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, res) +} diff --git a/internal/tezos/tezos.go b/internal/tezos/tezos.go index f44913e..f0a703d 100644 --- a/internal/tezos/tezos.go +++ b/internal/tezos/tezos.go @@ -31,7 +31,7 @@ type tezosConnector struct { blockListener *blockListener eventFilterPollingInterval time.Duration - client *rpc.Client + client rpc.RpcClient networkName string signatoryURL string diff --git a/internal/tezos/tezos_test.go b/internal/tezos/tezos_test.go index aca5703..4143c7e 100644 --- a/internal/tezos/tezos_test.go +++ b/internal/tezos/tezos_test.go @@ -22,9 +22,7 @@ func newTestConnector(t *testing.T) (context.Context, *tezosConnector, *tzrpcbac cc, err := NewTezosConnector(ctx, conf) assert.NoError(t, err) c := cc.(*tezosConnector) - // TODO: tzgo returns client as a structure with a state. - // Define, how to mock it - // c.client = mRPC + c.client = mRPC return ctx, c, mRPC, func() { done()