Skip to content

Commit

Permalink
Merge pull request #6 from kaleido-io/uts
Browse files Browse the repository at this point in the history
Add deploy contract unit tests
  • Loading branch information
nguyer authored Aug 15, 2022
2 parents 3b3313a + dc1c7bb commit 08fcc57
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 91 deletions.
114 changes: 114 additions & 0 deletions internal/ethereum/deploy_contract_prepare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright © 2022 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 ethereum

import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"strings"

"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-common/pkg/log"
"github.com/hyperledger/firefly-evmconnect/internal/msgs"
"github.com/hyperledger/firefly-signer/pkg/abi"
"github.com/hyperledger/firefly-signer/pkg/ethtypes"
"github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
)

func (c *ethConnector) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) (res *ffcapi.TransactionPrepareResponse, reason ffcapi.ErrorReason, err error) {

// Parse the input JSON data, to build the call data
callData, constructor, err := c.prepareDeployData(ctx, req)
if err != nil {
return nil, ffcapi.ErrorReasonInvalidInputs, err
}

// Build the base transaction object
tx, err := c.buildTx(ctx, txTypeDeployContract, req.From, "", req.Nonce, req.Gas, req.Value, callData)
if err != nil {
return nil, ffcapi.ErrorReasonInvalidInputs, err
}

if req.Gas, reason, err = c.ensureGasEstimate(ctx, tx, constructor, req.Gas); err != nil {
return nil, reason, err
}
log.L(ctx).Infof("Prepared deploy transaction dataLen=%d gas=%s", len(callData), req.Gas.Int())

return &ffcapi.TransactionPrepareResponse{
Gas: req.Gas,
TransactionData: ethtypes.HexBytes0xPrefix(callData).String(),
}, "", nil

}

func (c *ethConnector) prepareDeployData(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) ([]byte, *abi.Entry, error) {
// Parse the bytecode as a hex string, or fallback to Base64
var bytecodeString string
if err := req.Contract.Unmarshal(ctx, &bytecodeString); err != nil {
return nil, nil, i18n.NewError(ctx, msgs.MsgDecodeBytecodeFailed)
}
bytecode, err := hex.DecodeString(strings.TrimPrefix(bytecodeString, "0x"))
if err != nil {
bytecode, err = base64.StdEncoding.DecodeString(bytecodeString)
if err != nil {
return nil, nil, i18n.NewError(ctx, msgs.MsgDecodeBytecodeFailed)
}
}

// Parse the ABI
var a *abi.ABI
err = json.Unmarshal(req.Definition.Bytes(), &a)
if err != nil {
return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalABIFail, err)
}

// Find the constructor in the ABI
method := a.Constructor()
if method == nil {
// Constructors are optional, so if there is none, simply return the bytecode as the calldata
return bytecode, nil, nil
}

// Parse the params into the standard semantics of Go JSON unmarshalling, with []interface{}
ethParams := make([]interface{}, len(req.Params))
for i, p := range req.Params {
if p != nil {
err := json.Unmarshal([]byte(*p), &ethParams[i])
if err != nil {
return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalParamFail, i, err)
}
}
}

// Match the parameters to the ABI call data for the method.
// Note the FireFly ABI decoding package handles formatting errors / translation etc.
var callData []byte
paramValues, err := method.Inputs.ParseExternalDataCtx(ctx, ethParams)
if err == nil {
callData, err = paramValues.EncodeABIData()
}
if err != nil {
return nil, nil, err
}

// Concatenate bytecode and constructor args for deployment transaction
callData = append(bytecode, callData...)

return callData, method, err
}
240 changes: 240 additions & 0 deletions internal/ethereum/deploy_contract_prepare_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
// Copyright © 2022 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 ethereum

import (
"encoding/json"
"fmt"
"testing"

"github.com/hyperledger/firefly-common/pkg/fftypes"
"github.com/hyperledger/firefly-signer/pkg/ethtypes"
"github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

const samplePrepareDeployTX = `{
"ffcapi": {
"version": "v1.0.0",
"id": "904F177C-C790-4B01-BDF4-F2B4E52E607E",
"type": "DeployContract"
},
"from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8",
"to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771",
"gas": 1000000,
"nonce": "111",
"value": "12345678901234567890123456789",
"contract": "0xfeedbeef",
"definition": [{
"inputs": [
{
"internalType":" uint256",
"name": "x",
"type": "uint256"
}
],
"outputs":[],
"type":"constructor"
}],
"params": [ 4276993775 ]
}`

func TestDeployContractPrepareOkNoEstimate(t *testing.T) {

ctx, c, _, done := newTestConnector(t)
defer done()

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
assert.NoError(t, err)
res, reason, err := c.DeployContractPrepare(ctx, &req)

assert.NoError(t, err)
assert.Empty(t, reason)

assert.Equal(t, int64(1000000), res.Gas.Int64())

}

func TestDeployContractPrepareWithEstimateRevert(t *testing.T) {

ctx, c, mRPC, done := newTestConnector(t)
defer done()

mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_estimateGas", mock.Anything).Return(fmt.Errorf("pop"))
mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_call", mock.Anything, "latest").Run(
func(args mock.Arguments) {
*(args[1].(*ethtypes.HexBytes0xPrefix)) = ethtypes.MustNewHexBytes0xPrefix("0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000114d75707065747279206465746563746564000000000000000000000000000000")
},
).Return(nil)

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
assert.NoError(t, err)
req.Gas = nil
res, reason, err := c.DeployContractPrepare(ctx, &req)
assert.Regexp(t, "FF23021", err)
assert.Equal(t, ffcapi.ErrorReasonTransactionReverted, reason)
assert.Nil(t, res)

mRPC.AssertExpectations(t)

}

func TestDeployContractPrepareBadBytecode(t *testing.T) {

ctx, c, _, done := newTestConnector(t)
defer done()

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
req.Contract = fftypes.JSONAnyPtr(`! not a string containing bytecode`)
assert.NoError(t, err)
_, reason, err := c.DeployContractPrepare(ctx, &req)

assert.Regexp(t, "FF23047", err)
assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason)

}

func TestDeployContractPrepareBadBytecodeNotHex(t *testing.T) {

ctx, c, _, done := newTestConnector(t)
defer done()

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
req.Contract = fftypes.JSONAnyPtr(`"!hex"`)
assert.NoError(t, err)
_, reason, err := c.DeployContractPrepare(ctx, &req)

assert.Regexp(t, "FF23047", err)
assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason)

}

func TestDeployContractPrepareBadABIDefinition(t *testing.T) {

ctx, c, _, done := newTestConnector(t)
defer done()

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
req.Definition = fftypes.JSONAnyPtr(`[`)
assert.NoError(t, err)
_, reason, err := c.DeployContractPrepare(ctx, &req)

assert.Regexp(t, "FF23013", err)
assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason)

}

func TestDeployContractPrepareEstimateNoConstructor(t *testing.T) {

ctx, c, mRPC, done := newTestConnector(t)
defer done()

mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_estimateGas", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
*(args[1].(*ethtypes.HexInteger)) = *ethtypes.NewHexInteger64(12345)
})

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
assert.NoError(t, err)
req.Definition = fftypes.JSONAnyPtr(`[]`)
req.Gas = nil
res, reason, err := c.DeployContractPrepare(ctx, &req)

assert.NoError(t, err)
assert.Empty(t, reason)

fGasEstimate, _ := c.gasEstimationFactor.Float64()
assert.Equal(t, int64(float64(12345)*fGasEstimate), res.Gas.Int64())

mRPC.AssertExpectations(t)

}

func TestDeployContractPrepareBadParamsJSON(t *testing.T) {

ctx, c, _, done := newTestConnector(t)
defer done()

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
req.Params = []*fftypes.JSONAny{fftypes.JSONAnyPtr(`"!wrong`)}
assert.NoError(t, err)
_, reason, err := c.DeployContractPrepare(ctx, &req)

assert.Regexp(t, "FF23014", err)
assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason)

}

func TestDeployContractPrepareBadParamType(t *testing.T) {

ctx, c, _, done := newTestConnector(t)
defer done()

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
req.Params = []*fftypes.JSONAny{fftypes.JSONAnyPtr(`"!wrong"`)}
assert.NoError(t, err)
_, reason, err := c.DeployContractPrepare(ctx, &req)

assert.Regexp(t, "FF22030", err)
assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason)

}

func TestDeployContractPrepareBadFrom(t *testing.T) {

ctx, c, _, done := newTestConnector(t)
defer done()

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
req.From = "!not an address"
assert.NoError(t, err)
_, reason, err := c.DeployContractPrepare(ctx, &req)

assert.Regexp(t, "FF23019", err)
assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason)

}

func TestDeployContractPrepareBadABIType(t *testing.T) {

ctx, c, _, done := newTestConnector(t)
defer done()

var req ffcapi.ContractDeployPrepareRequest
err := json.Unmarshal([]byte(samplePrepareDeployTX), &req)
req.Definition = fftypes.JSONAnyPtr(`[{
"type": "constructor",
"inputs": [{
"type": "!wrong"
}]
}]`)
assert.NoError(t, err)
_, reason, err := c.DeployContractPrepare(ctx, &req)

assert.Regexp(t, "FF22025", err)
assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason)

}
5 changes: 4 additions & 1 deletion internal/ethereum/event_stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,10 @@ func TestCatchupThenRejoinLeadGroup(t *testing.T) {
// Confirm the listener joins the group
started := time.Now()
for {
assert.True(t, time.Since(started) < 5*time.Second)
t.Logf("Catchup=%t HeadBlock=%d", l.catchup, es.headBlock)
if time.Since(started) > 1*time.Second {
assert.Fail(t, "Never exited catchup")
}
if l.catchup {
time.Sleep(1 * time.Millisecond)
continue
Expand Down
Loading

0 comments on commit 08fcc57

Please sign in to comment.