diff --git a/go.mod b/go.mod index 1a1f99e..9a8ebe7 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ toolchain go1.21.6 require ( github.com/hashicorp/golang-lru v1.0.2 - github.com/hyperledger/firefly-common v1.4.8 - github.com/hyperledger/firefly-signer v1.1.13 - github.com/hyperledger/firefly-transaction-manager v1.3.15 + github.com/hyperledger/firefly-common v1.4.9 + github.com/hyperledger/firefly-signer v1.1.14 + github.com/hyperledger/firefly-transaction-manager v1.3.16 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 8c3c0bd..69af54d 100644 --- a/go.sum +++ b/go.sum @@ -100,12 +100,12 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/hyperledger/firefly-common v1.4.8 h1:0o1Qp1c5YzQo8nbnX+gAo9SVd2tR4Z9U2t8Y4zEzyaA= -github.com/hyperledger/firefly-common v1.4.8/go.mod h1:dXewcVMFNON2SvQ1UPvu64OWUt77+M3p8qy61lT1kE4= -github.com/hyperledger/firefly-signer v1.1.13 h1:eiHjc6HPRG8AzXUCUgm51qqX1I9BokiuiiqJ89XwK4M= -github.com/hyperledger/firefly-signer v1.1.13/go.mod h1:pK6kivzBFSue3zpJSQpH67VasnLLbwBJOBUNv0zHbRA= -github.com/hyperledger/firefly-transaction-manager v1.3.15 h1:IyWIId+uytqjIRMxROk5OqOcdHMzJFGFKpQQybiISOU= -github.com/hyperledger/firefly-transaction-manager v1.3.15/go.mod h1:N3BoHh8+dWG710oQKuNiXmJNEOBBeLTsQ8GpZ41vhog= +github.com/hyperledger/firefly-common v1.4.9 h1:PfPZ73FN8WUoPl8iF8ud00B8476+jmqXHHi94w0Krbc= +github.com/hyperledger/firefly-common v1.4.9/go.mod h1:dXewcVMFNON2SvQ1UPvu64OWUt77+M3p8qy61lT1kE4= +github.com/hyperledger/firefly-signer v1.1.14 h1:gSGwdBHTLPchGlmLOKk2Y2nawfMhlH2CDm2owt0lIUE= +github.com/hyperledger/firefly-signer v1.1.14/go.mod h1:Xj2PF6y8Ce26jX38ch0KasNnnZCSyzcwyLSv8NN+7JA= +github.com/hyperledger/firefly-transaction-manager v1.3.16 h1:rW6rptO4LcOeUYVOMTyvsfPcOzNLBpmBTj4T6n0/vpY= +github.com/hyperledger/firefly-transaction-manager v1.3.16/go.mod h1:UT4Cijjsz5AqiXa9H3GUiT2vm2Hq1wEgT0n/shsYZp0= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/ethereum/blocklistener.go b/internal/ethereum/blocklistener.go index 99ff16a..87199e6 100644 --- a/internal/ethereum/blocklistener.go +++ b/internal/ethereum/blocklistener.go @@ -230,6 +230,7 @@ func (bl *blockListener) listenLoop() { failCount++ continue } + log.L(bl.ctx).Debugf("Block filter received new block hashes: %+v", blockHashes) update := &ffcapi.BlockHashEvent{GapPotential: gapPotential, Created: fftypes.Now()} var notifyPos *list.Element @@ -260,7 +261,7 @@ func (bl *blockListener) listenLoop() { default: candidate := bl.reconcileCanonicalChain(bi) // Check this is the lowest position to notify from - if candidate != nil && (notifyPos == nil || candidate.Value.(*minimalBlockInfo).number < notifyPos.Value.(*minimalBlockInfo).number) { + if candidate != nil && (notifyPos == nil || candidate.Value.(*minimalBlockInfo).number <= notifyPos.Value.(*minimalBlockInfo).number) { notifyPos = candidate } } diff --git a/internal/ethereum/blocklistener_test.go b/internal/ethereum/blocklistener_test.go index 6d5a47b..36fc0a0 100644 --- a/internal/ethereum/blocklistener_test.go +++ b/internal/ethereum/blocklistener_test.go @@ -391,6 +391,392 @@ func TestBlockListenerOKDuplicates(t *testing.T) { } +func TestBlockListenerReorgKeepLatestHeadInSameBatch(t *testing.T) { + + _, c, mRPC, done := newTestConnector(t) + bl := c.blockListener + bl.blockPollingInterval = 1 * time.Microsecond + + block1000Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) // parent + block1001HashA := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1001HashB := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1002Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1003Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_blockNumber").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*ethtypes.HexInteger) + *hbh = *ethtypes.NewHexInteger64(1000) + }).Once() + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_newBlockFilter").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*string) + *hbh = "filter_id1" + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", "filter_id1").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*[]ethtypes.HexBytes0xPrefix) + *hbh = []ethtypes.HexBytes0xPrefix{ + block1001HashA, + block1001HashB, + block1002Hash, + block1003Hash, + } + }).Once() + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything).Return(nil) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1001HashA.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1001), + Hash: block1001HashA, + ParentHash: block1000Hash, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1001HashB.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1001), + Hash: block1001HashB, + ParentHash: block1000Hash, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1002Hash.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1002), + Hash: block1002Hash, + ParentHash: block1001HashB, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1003Hash.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1003), + Hash: block1003Hash, + ParentHash: block1002Hash, + } + }) + + updates := make(chan *ffcapi.BlockHashEvent) + bl.addConsumer(&blockUpdateConsumer{ + id: fftypes.NewUUID(), + ctx: context.Background(), + updates: updates, + }) + + bu := <-updates + assert.Equal(t, []string{ + block1001HashB.String(), + block1002Hash.String(), + block1003Hash.String(), + }, bu.BlockHashes) + + done() + <-bl.listenLoopDone + + assert.Equal(t, int64(1003), bl.highestBlock) + + mRPC.AssertExpectations(t) +} + +func TestBlockListenerReorgKeepLatestHeadInSameBatchValidHashFirst(t *testing.T) { + + _, c, mRPC, done := newTestConnector(t) + bl := c.blockListener + bl.blockPollingInterval = 1 * time.Microsecond + + block1000Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) // parent + block1001HashA := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1001HashB := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1002Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1003Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_blockNumber").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*ethtypes.HexInteger) + *hbh = *ethtypes.NewHexInteger64(1000) + }).Once() + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_newBlockFilter").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*string) + *hbh = "filter_id1" + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", "filter_id1").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*[]ethtypes.HexBytes0xPrefix) + *hbh = []ethtypes.HexBytes0xPrefix{ + block1001HashB, // valid hash is in the front of the array, so will need to re-build the chain + block1001HashA, + block1002Hash, + block1003Hash, + } + }).Once() + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything).Return(nil) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().Int64() == 1001 + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1001), + Hash: block1001HashB, + ParentHash: block1000Hash, + } + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().Int64() == 1002 + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1002), + Hash: block1002Hash, + ParentHash: block1001HashB, + } + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().Int64() == 1003 + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1003), + Hash: block1003Hash, + ParentHash: block1002Hash, + } + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().Int64() == 1004 // not found + }), false).Return(nil) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1001HashA.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1001), + Hash: block1001HashA, + ParentHash: block1000Hash, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1001HashB.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1001), + Hash: block1001HashB, + ParentHash: block1000Hash, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1002Hash.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1002), + Hash: block1002Hash, + ParentHash: block1001HashB, + } + }) + updates := make(chan *ffcapi.BlockHashEvent) + bl.addConsumer(&blockUpdateConsumer{ + id: fftypes.NewUUID(), + ctx: context.Background(), + updates: updates, + }) + + bu := <-updates + assert.Equal(t, []string{ + block1001HashB.String(), + block1002Hash.String(), + block1003Hash.String(), + }, bu.BlockHashes) + + done() + <-bl.listenLoopDone + + assert.Equal(t, int64(1003), bl.highestBlock) + + mRPC.AssertExpectations(t) +} + +func TestBlockListenerReorgKeepLatestMiddleInSameBatch(t *testing.T) { + + _, c, mRPC, done := newTestConnector(t) + bl := c.blockListener + bl.blockPollingInterval = 1 * time.Microsecond + + block1000Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) // parent + block1001Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1003Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1002HashA := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1002HashB := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_blockNumber").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*ethtypes.HexInteger) + *hbh = *ethtypes.NewHexInteger64(1000) + }).Once() + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_newBlockFilter").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*string) + *hbh = "filter_id1" + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", "filter_id1").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*[]ethtypes.HexBytes0xPrefix) + *hbh = []ethtypes.HexBytes0xPrefix{ + block1001Hash, + block1002HashA, + block1002HashB, + block1003Hash, + } + }).Once() + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything).Return(nil) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1001Hash.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1001), + Hash: block1001Hash, + ParentHash: block1000Hash, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1002HashA.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1002), + Hash: block1002HashA, + ParentHash: block1001Hash, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1002HashB.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1002), + Hash: block1002HashB, + ParentHash: block1001Hash, + } + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1003Hash.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1003), + Hash: block1003Hash, + ParentHash: block1002HashB, + } + }) + updates := make(chan *ffcapi.BlockHashEvent) + bl.addConsumer(&blockUpdateConsumer{ + id: fftypes.NewUUID(), + ctx: context.Background(), + updates: updates, + }) + + bu := <-updates + assert.Equal(t, []string{ + block1001Hash.String(), + block1002HashB.String(), + block1003Hash.String(), + }, bu.BlockHashes) + + done() + <-bl.listenLoopDone + + assert.Equal(t, int64(1003), bl.highestBlock) + + mRPC.AssertExpectations(t) +} + +func TestBlockListenerReorgKeepLatestTailInSameBatch(t *testing.T) { + + _, c, mRPC, done := newTestConnector(t) + bl := c.blockListener + bl.blockPollingInterval = 1 * time.Microsecond + + block1000Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) // parent + block1001Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1003HashB := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1002Hash := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + block1003HashA := ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_blockNumber").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*ethtypes.HexInteger) + *hbh = *ethtypes.NewHexInteger64(1000) + }).Once() + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_newBlockFilter").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*string) + *hbh = "filter_id1" + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", "filter_id1").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*[]ethtypes.HexBytes0xPrefix) + *hbh = []ethtypes.HexBytes0xPrefix{ + block1001Hash, + block1002Hash, + block1003HashA, + block1003HashB, + } + }).Once() + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything).Return(nil) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1001Hash.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1001), + Hash: block1001Hash, + ParentHash: block1000Hash, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1002Hash.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1002), + Hash: block1002Hash, + ParentHash: block1001Hash, + } + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1003HashA.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1003), + Hash: block1003HashA, + ParentHash: block1002Hash, + } + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == block1003HashB.String() + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1003), + Hash: block1003HashB, + ParentHash: block1002Hash, + } + }) + updates := make(chan *ffcapi.BlockHashEvent) + bl.addConsumer(&blockUpdateConsumer{ + id: fftypes.NewUUID(), + ctx: context.Background(), + updates: updates, + }) + + bu := <-updates + assert.Equal(t, []string{ + block1001Hash.String(), + block1002Hash.String(), + block1003HashB.String(), + }, bu.BlockHashes) + + done() + <-bl.listenLoopDone + + assert.Equal(t, int64(1003), bl.highestBlock) + + mRPC.AssertExpectations(t) +} + func TestBlockListenerReorgReplaceTail(t *testing.T) { _, c, mRPC, done := newTestConnectorWithNoBlockerFilterDefaultMocks(t) diff --git a/internal/ethereum/deploy_contract_prepare.go b/internal/ethereum/deploy_contract_prepare.go index b53b387..bf4c2bc 100644 --- a/internal/ethereum/deploy_contract_prepare.go +++ b/internal/ethereum/deploy_contract_prepare.go @@ -95,7 +95,7 @@ func (c *ethConnector) prepareDeployData(ctx context.Context, req *ffcapi.Contra ethParams := make([]interface{}, len(req.Params)) for i, p := range req.Params { if p != nil { - err := json.Unmarshal([]byte(*p), ðParams[i]) + err := p.Unmarshal(ctx, ðParams[i]) if err != nil { return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalParamFail, i, err) } diff --git a/internal/ethereum/deploy_contract_prepare_test.go b/internal/ethereum/deploy_contract_prepare_test.go index 61661e0..f8eb103 100644 --- a/internal/ethereum/deploy_contract_prepare_test.go +++ b/internal/ethereum/deploy_contract_prepare_test.go @@ -18,6 +18,7 @@ package ethereum import ( "encoding/json" + "strings" "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -39,19 +40,91 @@ const samplePrepareDeployTX = `{ "gas": 1000000, "nonce": "111", "value": "12345678901234567890123456789", - "contract": "0xfeedbeef", + "contract": "0xdeadbeef", "definition": [{ "inputs": [ { "internalType":" uint256", "name": "x", "type": "uint256" - } + }, + { + "internalType":" address", + "name": "y", + "type": "address" + }, + { + "internalType":" string", + "name": "z", + "type": "string" + } ], "outputs":[], "type":"constructor" }], - "params": [ 4276993775 ] + "params": [ 4276993775, "0x5f906824E562B6a0F278D910D388728b833a43bB", "some-text" ] +}` + +const samplePrepareDeployTXLargeInputParams = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "DeployContract" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "nonce": "111", + "value": "12345678901234567890123456789", + "contract": "0xdeadbeef", + "definition": [{ + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType":" string", + "name": "y", + "type": "string" + } + ], + "outputs":[], + "type":"constructor" + }], + "params": [ 10000000000000000000000001, "some-text" ] +}` + +const samplePrepareDeployTXScientificNotation = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "DeployContract" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "nonce": "111", + "value": "12345678901234567890123456789", + "contract": "0xdeadbeef", + "definition": [{ + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType":" string", + "name": "y", + "type": "string" + } + ], + "outputs":[], + "type":"constructor" + }], + "params": [ 1.0000000000000000000000001e+25, "some-text" ] }` func TestDeployContractPrepareOkNoEstimate(t *testing.T) { @@ -63,12 +136,53 @@ func TestDeployContractPrepareOkNoEstimate(t *testing.T) { 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()) + + // Basic check that our input param 4276993775 is in the TX data + assert.True(t, strings.Contains(res.TransactionData, "feedbeef")) + // Basic check that our input param "some-text" is in the TX data + assert.True(t, strings.Contains(res.TransactionData, strings.ToLower("736f6d652d74657874"))) + // Basic check that our input param address is in the TX data + assert.True(t, strings.Contains(res.TransactionData, strings.ToLower("5f906824E562B6a0F278D910D388728b833a43bB"))) +} + +func TestDeployContractPrepareOkLargeInputParam(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTXLargeInputParams), &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()) + // Basic check that our input param 10000000000000000000000001 is in the TX data + assert.True(t, strings.Contains(res.TransactionData, "84595161401484a000001")) + // Basic check that our input param "some-text" is in the TX data + assert.True(t, strings.Contains(res.TransactionData, "736f6d652d74657874")) +} + +func TestDeployContractPrepareOkScientificNotationParam(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTXScientificNotation), &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()) + // Basic check that our input param 1.0000000000000000000000001e+25 is in the TX data + assert.True(t, strings.Contains(res.TransactionData, "84595161401484a000001")) + // Basic check that our input param "some-text" is in the TX data + assert.True(t, strings.Contains(res.TransactionData, "736f6d652d74657874")) } func TestDeployContractPrepareWithEstimateRevert(t *testing.T) { @@ -209,7 +323,7 @@ func TestDeployContractPrepareBadParamType(t *testing.T) { var req ffcapi.ContractDeployPrepareRequest err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) - req.Params = []*fftypes.JSONAny{fftypes.JSONAnyPtr(`"!wrong"`)} + req.Params = []*fftypes.JSONAny{fftypes.JSONAnyPtr(`"!wrong"`), fftypes.JSONAnyPtr(`"0x90eB678C3586103805a676d21721Cc6883a6c3AE"`), fftypes.JSONAnyPtr(`"helloworld"`)} assert.NoError(t, err) _, reason, err := c.DeployContractPrepare(ctx, &req) diff --git a/internal/ethereum/prepare_transaction.go b/internal/ethereum/prepare_transaction.go index 502b5ad..96fabcb 100644 --- a/internal/ethereum/prepare_transaction.go +++ b/internal/ethereum/prepare_transaction.go @@ -108,7 +108,7 @@ func (c *ethConnector) prepareCallData(ctx context.Context, req *ffcapi.Transact ethParams := make([]interface{}, len(req.Params)) for i, p := range req.Params { if p != nil { - err := json.Unmarshal([]byte(*p), ðParams[i]) + err := p.Unmarshal(ctx, ðParams[i]) if err != nil { return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalParamFail, i, err) } diff --git a/internal/ethereum/prepare_transaction_test.go b/internal/ethereum/prepare_transaction_test.go index ea78b8a..a54c07c 100644 --- a/internal/ethereum/prepare_transaction_test.go +++ b/internal/ethereum/prepare_transaction_test.go @@ -19,6 +19,7 @@ package ethereum import ( "context" "encoding/json" + "strings" "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -155,6 +156,70 @@ const samplePrepareTXBadErrors = `{ "errors": [false] }` +const samplePrepareTXHugeNumberParam = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "prepare_transaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "nonce": "222", + "method": { + "inputs": [], + "name":"do", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "method": { + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "name":"set", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "params": [ 10000000000000000000000001 ] +}` + +const samplePrepareTXScientificNumberParam = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "prepare_transaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "nonce": "222", + "method": { + "inputs": [], + "name":"do", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "method": { + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "name":"set", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "params": [ 1.0000000000000000000000002e+25 ] +}` + func TestPrepareTransactionOkNoEstimate(t *testing.T) { ctx, c, _, done := newTestConnector(t) @@ -172,6 +237,60 @@ func TestPrepareTransactionOkNoEstimate(t *testing.T) { } +func TestPrepareTransactionOkHugeNumberParam(t *testing.T) { + + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas", + mock.MatchedBy(func(tx *ethsigner.Transaction) bool { + assert.Equal(t, "0x60fe47b1000000000000000000000000000000000000000000084595161401484a000001", tx.Data.String()) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + args[1].(*ethtypes.HexInteger).BigInt().SetString("12345", 10) + }) + + var req ffcapi.TransactionPrepareRequest + err := json.Unmarshal([]byte(samplePrepareTXHugeNumberParam), &req) + assert.NoError(t, err) + res, reason, err := c.TransactionPrepare(ctx, &req) + + assert.NoError(t, err) + assert.Empty(t, reason) + + // Basic check that our input param 10000000000000000000000001 is in the TX data + assert.True(t, strings.Contains(res.TransactionData, "84595161401484a000001")) +} + +func TestPrepareTransactionOkScientificNumberParam(t *testing.T) { + + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas", + mock.MatchedBy(func(tx *ethsigner.Transaction) bool { + assert.Equal(t, "0x60fe47b1000000000000000000000000000000000000000000084595161401484a000002", tx.Data.String()) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + args[1].(*ethtypes.HexInteger).BigInt().SetString("12345", 10) + }) + + var req ffcapi.TransactionPrepareRequest + err := json.Unmarshal([]byte(samplePrepareTXScientificNumberParam), &req) + assert.NoError(t, err) + res, reason, err := c.TransactionPrepare(ctx, &req) + + assert.NoError(t, err) + assert.Empty(t, reason) + + // Basic check that our input param 1.0000000000000000000000002e+25 is in the TX data + assert.True(t, strings.Contains(res.TransactionData, "84595161401484a000002")) +} + func TestPrepareTransactionWithEstimate(t *testing.T) { ctx, c, mRPC, done := newTestConnector(t)