Repository: cosmos/relayer
Pull request #1014 (2022-10-10 15:43:41)

Test/event parser
## Description

The IBC Event parser is responsible for parsing and converting the IBC events into IBC messages understandable for the relayer chain processor. In order to increate the reliability, test cases wit sample data are added to the substrate package.

## Added Tests

- [x] `TestParsePacket`
- [x] `TestParseClient`
- [x] `TestParseClientUpdate`
- [x] `TestParseChannel`
- [x] `TestParseConnection`
- [x] `TestParseEvents`

## Fixed Bugs

- [x] Wrong casting mechanism for `uint64` and `uint32`
- [x] Make `header` of the `ClientUpdateInfo` as optional property
- [x] Timestamp parsing function


Patch:
diff --git a/go.mod b/go.mod
index c0d7868..60bc6db 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.12.2
+	github.com/spf13/cast v1.5.0
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/viper v1.12.0
 	github.com/strangelove-ventures/lens v0.5.2-0.20220822201013-1e7ffd450f20
@@ -146,7 +147,6 @@ require (
 	github.com/rs/cors v1.8.2 // indirect
 	github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa // indirect
 	github.com/spf13/afero v1.8.2 // indirect
-	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.4.0 // indirect
diff --git a/relayer/chains/substrate/event_parser.go b/relayer/chains/substrate/event_parser.go
index 264ee73..8a39d17 100644
--- a/relayer/chains/substrate/event_parser.go
+++ b/relayer/chains/substrate/event_parser.go
@@ -2,12 +2,13 @@ package substrate
 
 import (
 	"fmt"
-	"strconv"
 
 	rpcclienttypes "github.com/ComposableFi/go-substrate-rpc-client/v4/types"
 	beefyclienttypes "github.com/ComposableFi/ics11-beefy/types"
 	clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types"
+	"github.com/cosmos/ibc-go/v5/modules/core/exported"
 	"github.com/cosmos/relayer/v2/relayer/provider"
+	"github.com/spf13/cast"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
 )
@@ -19,18 +20,18 @@ type ibcMessage struct {
 }
 
 type ibcMessageInfo interface {
-	parseAttrs(log *zap.Logger, attrs interface{})
+	parseAttrs(log *zap.Logger, attrs any)
 	MarshalLogObject(enc zapcore.ObjectEncoder) error
 }
 
 // alias for the interface map
-type ibcEventQueryItem (map[string]interface{})
+type ibcEventQueryItem = map[string]any
 
 // substrate ibc events endpoint returns a list of events
 // relayted to different transaction, so we need
 // to define a unique key
 type ibcPacketKey struct {
-	sequence   uint64
+	sequence   string
 	srcChannel string
 	srcPort    string
 	dstChannel string
@@ -40,13 +41,12 @@ type ibcPacketKey struct {
 // ibcMessagesFromEvents parses all events of a certain height to find IBC messages
 func (scp *SubstrateChainProcessor) ibcMessagesFromEvents(
 	ibcEvents rpcclienttypes.IBCEventsQueryResult,
-	height uint64,
 ) (messages []ibcMessage) {
 
 	packetAccumulator := make(map[ibcPacketKey]*packetInfo)
 	for i := 0; i < len(ibcEvents); i++ {
 
-		info, eventType := scp.parseEvent(ibcEvents[i], height, packetAccumulator)
+		info, eventType := scp.parseEvent(ibcEvents[i], packetAccumulator)
 		if info == nil {
 			// Not an IBC message, don't need to log here
 			// event is write acknowledement, so receive packet will be processed by accumulator
@@ -73,7 +73,6 @@ func (scp *SubstrateChainProcessor) ibcMessagesFromEvents(
 
 func (scp *SubstrateChainProcessor) parseEvent(
 	event ibcEventQueryItem,
-	height uint64,
 	packetAccumulator map[ibcPacketKey]*packetInfo,
 ) (info ibcMessageInfo, eventType string) {
 
@@ -113,21 +112,21 @@ func (scp *SubstrateChainProcessor) parseEvent(
 
 			_, exists := packetAccumulator[accumKey]
 			if !exists {
-				packetAccumulator[accumKey] = &packetInfo{Height: height}
+				packetAccumulator[accumKey] = new(packetInfo)
 			}
 
 			packetAccumulator[accumKey].parseAttrs(scp.log, data)
 
 		case OpenInitConnection, OpenTryConnection, OpenAckConnection, OpenConfirmConnection:
 
-			con := &connectionInfo{Height: height}
+			con := new(connectionInfo)
 			con.parseAttrs(scp.log, data)
 			info = con
 
 			eventType = intoIBCEventType(eType)
 
 		case OpenInitChannel, OpenTryChannel, OpenAckChannel, OpenConfirmChannel, CloseInitChannel, CloseConfirmChannel:
-			chann := &channelInfo{Height: height}
+			chann := new(channelInfo)
 			chann.parseAttrs(scp.log, data)
 			info = chann
 
@@ -143,9 +142,9 @@ func (scp *SubstrateChainProcessor) parseEvent(
 }
 
 // returns the unique key for packet accumulator cache
-func genAccumKey(data interface{}) ibcPacketKey {
+func genAccumKey(data any) ibcPacketKey {
 	return ibcPacketKey{
-		sequence:   data.(ibcEventQueryItem)["sequence"].(uint64),
+		sequence:   cast.ToString(data.(ibcEventQueryItem)["sequence"].(float64)),
 		srcChannel: data.(ibcEventQueryItem)["source_channel"].(string),
 		srcPort:    data.(ibcEventQueryItem)["source_port"].(string),
 		dstChannel: data.(ibcEventQueryItem)["destination_channel"].(string),
@@ -178,22 +177,28 @@ func (cl *clientInfo) MarshalLogObject(enc zapcore.ObjectEncoder) error {
 }
 
 // parseAttrs parses the attributes of client info
-func (cl *clientInfo) parseAttrs(log *zap.Logger, attributes interface{}) {
+func (cl *clientInfo) parseAttrs(log *zap.Logger, attributes any) {
 	attrs := attributes.(ibcEventQueryItem)
 
 	var err error
 	if cl.height, err = parseHeight(attrs["height"]); err != nil {
-		log.Error("error parsing client consensus height: ",
+		log.Error("Error parsing client consensus height",
 			zap.Error(err),
 		)
 		return
 	}
 
 	cl.clientID = attrs["client_id"].(string)
-	cl.clientType = attrs["client_type"].(uint32)
+
+	if cl.clientType, err = cast.ToUint32E(attrs["client_type"].(string)); err != nil {
+		log.Error("Error parsing client type",
+			zap.Error(err),
+		)
+		return
+	}
 
 	if cl.consensusHeight, err = parseHeight(attrs["consensus_height"]); err != nil {
-		log.Error("error parsing client consensus height: ",
+		log.Error("error parsing client consensus height",
 			zap.Error(err),
 		)
 		return
@@ -217,14 +222,16 @@ func (clu *clientUpdateInfo) MarshalLogObject(enc zapcore.ObjectEncoder) error {
 }
 
 // parseAttrs parses the attributes of client update info
-func (clu *clientUpdateInfo) parseAttrs(log *zap.Logger, attributes interface{}) {
+func (clu *clientUpdateInfo) parseAttrs(log *zap.Logger, attributes any) {
 	attrs := attributes.(ibcEventQueryItem)
 
 	clientInfo := new(clientInfo)
 	clientInfo.parseAttrs(log, attrs["common"])
 	clu.common = *clientInfo
 
-	clu.header = parseHeader(attrs["header"])
+	if h, headerFound := attrs["header"]; headerFound {
+		clu.header = parseHeader(h)
+	}
 }
 
 // alias type to the provider types, used for adding parser methods
@@ -241,29 +248,56 @@ func (pkt *packetInfo) MarshalLogObject(enc zapcore.ObjectEncoder) error {
 }
 
 // parseAttrs parses the attributes of packet info
-func (pkt *packetInfo) parseAttrs(log *zap.Logger, attributes interface{}) {
+func (pkt *packetInfo) parseAttrs(log *zap.Logger, attributes any) {
 	attrs := attributes.(ibcEventQueryItem)
 
-	pkt.Sequence = attrs["sequence"].(uint64)
+	var err error
+
+	var height exported.Height
+	if height, err = parseHeight(attrs["height"]); err != nil {
+		log.Error("Error parsing connection height",
+			zap.Error(err),
+		)
+		return
+	}
+	pkt.Height = height.GetRevisionHeight()
+
+	pkt.Sequence = cast.ToUint64(attrs["sequence"].(float64))
 	pkt.SourcePort = attrs["source_port"].(string)
 	pkt.SourceChannel = attrs["source_channel"].(string)
 	pkt.DestPort = attrs["destination_port"].(string)
 	pkt.DestChannel = attrs["destination_channel"].(string)
-	pkt.Data = attrs["data"].([]byte)
 
-	var err error
+	if pkt.Data, err = rpcclienttypes.HexDecodeString(attrs["data"].(string)); err != nil {
+		log.Error("Error parsing packet data",
+			zap.Error(err),
+		)
+		return
+	}
+
 	if pkt.TimeoutHeight, err = parseHeight(attrs["timeout_height"]); err != nil {
-		log.Error("error parsing packet height: ",
+		log.Error("error parsing packet height",
+			zap.Error(err),
+		)
+		return
+	}
+
+	if pkt.TimeoutTimestamp, err = parseTimestamp(attrs["timeout_timestamp"]); err != nil {
+		log.Error("Error parsing packet timeout timestamp",
 			zap.Error(err),
 		)
 		return
 	}
 
-	pkt.TimeoutTimestamp = attrs["timeout_timestamp"].(uint64)
+	ack, ackFound := attrs["ack"]
+	if ackFound {
 
-	ack, found := attrs["ack"]
-	if found {
-		pkt.Ack = ack.([]byte)
+		if pkt.Ack, err = rpcclienttypes.HexDecodeString(ack.(string)); err != nil {
+			log.Error("Error parsing ack data",
+				zap.Error(err),
+			)
+			return
+		}
 	}
 
 	// TODO: how to populate order
@@ -286,9 +320,20 @@ func (ch *channelInfo) MarshalLogObject(enc zapcore.ObjectEncoder) error {
 }
 
 // parseAttrs parses the attributes of channel info
-func (ch *channelInfo) parseAttrs(log *zap.Logger, attributes interface{}) {
+func (ch *channelInfo) parseAttrs(log *zap.Logger, attributes any) {
 	attrs := attributes.(ibcEventQueryItem)
 
+	var err error
+
+	var height exported.Height
+	if height, err = parseHeight(attrs["height"]); err != nil {
+		log.Error("Error parsing channel height",
+			zap.Error(err),
+		)
+		return
+	}
+	ch.Height = height.GetRevisionHeight()
+
 	ch.PortID = attrs["port_id"].(string)
 	ch.ChannelID = attrs["channel_id"].(string)
 	ch.CounterpartyPortID = attrs["counterparty_port_id"].(string)
@@ -309,25 +354,36 @@ func (con *connectionInfo) MarshalLogObject(enc zapcore.ObjectEncoder) error {
 }
 
 // parseAttrs parses the attributes of connection info
-func (con *connectionInfo) parseAttrs(log *zap.Logger, attributes interface{}) {
+func (con *connectionInfo) parseAttrs(log *zap.Logger, attributes any) {
 	attrs := attributes.(ibcEventQueryItem)
 
+	var err error
+
+	var height exported.Height
+	if height, err = parseHeight(attrs["height"]); err != nil {
+		log.Error("Error parsing connection height",
+			zap.Error(err),
+		)
+		return
+	}
+	con.Height = height.GetRevisionHeight()
+
 	con.ConnID = attrs["connection_id"].(string)
 	con.ClientID = attrs["client_id"].(string)
 	con.CounterpartyClientID = attrs["counterparty_client_id"].(string)
 	con.CounterpartyConnID = attrs["counterparty_connection_id"].(string)
 }
 
-// parses the attributes of ibc core client type
-func parseHeight(i interface{}) (clienttypes.Height, error) {
+// parses the height from an input interface
+func parseHeight(i any) (clienttypes.Height, error) {
 	height := i.(ibcEventQueryItem)
 
-	revisionNumber, err := strconv.ParseUint(height["revision_number"].(string), 10, 64)
+	revisionNumber, err := cast.ToUint64E(height["revision_number"].(float64))
 	if err != nil {
 		return clienttypes.Height{}, fmt.Errorf("error parsing revision number: %s", err)
 	}
 
-	revisionHeight, err := strconv.ParseUint(height["revision_height"].(string), 10, 64)
+	revisionHeight, err := cast.ToUint64E(height["revision_height"].(float64))
 	if err != nil {
 		return clienttypes.Height{}, fmt.Errorf("error parsing revision height: %s", err)
 	}
@@ -338,8 +394,19 @@ func parseHeight(i interface{}) (clienttypes.Height, error) {
 	}, nil
 }
 
+// parses timestamp from an input interface
+func parseTimestamp(i any) (uint64, error) {
+	timestamp := i.(ibcEventQueryItem)
+
+	t, err := cast.ToTimeE(timestamp["time"].(string))
+	if err != nil {
+		return 0, fmt.Errorf("error parsing timestamp %s", err)
+	}
+	return uint64(t.UnixNano()), nil
+}
+
 // parses beefy header
-func parseHeader(i interface{}) (res beefyclienttypes.Header) {
+func parseHeader(i any) (res beefyclienttypes.Header) {
 	attrs := i.(ibcEventQueryItem)
 
 	res = beefyclienttypes.Header{
@@ -351,7 +418,7 @@ func parseHeader(i interface{}) (res beefyclienttypes.Header) {
 }
 
 // parses parachain header and mmr proofs
-func parseParachainHeaderWithProof(i interface{}) (res *beefyclienttypes.ParachainHeadersWithProof) {
+func parseParachainHeaderWithProof(i any) (res *beefyclienttypes.ParachainHeadersWithProof) {
 	attrs := i.(ibcEventQueryItem)
 
 	res = &beefyclienttypes.ParachainHeadersWithProof{
@@ -364,7 +431,7 @@ func parseParachainHeaderWithProof(i interface{}) (res *beefyclienttypes.Paracha
 }
 
 // parses parachain header attributes
-func parseParachainHeaders(i interface{}) (res []*beefyclienttypes.ParachainHeader) {
+func parseParachainHeaders(i any) (res []*beefyclienttypes.ParachainHeader) {
 	attrs := i.(rpcclienttypes.IBCEventsQueryResult)
 	res = []*beefyclienttypes.ParachainHeader{}
 
@@ -387,7 +454,7 @@ func parseParachainHeaders(i interface{}) (res []*beefyclienttypes.ParachainHead
 }
 
 // parses mmr update proof attributes
-func parseUpdateProofs(i interface{}) (res *beefyclienttypes.MMRUpdateProof) {
+func parseUpdateProofs(i any) (res *beefyclienttypes.MMRUpdateProof) {
 	attrs := i.(ibcEventQueryItem)
 
 	res = &beefyclienttypes.MMRUpdateProof{
@@ -402,7 +469,7 @@ func parseUpdateProofs(i interface{}) (res *beefyclienttypes.MMRUpdateProof) {
 }
 
 // parses partial mmr tree leaf attributes
-func parsePartialMMRLeaf(i interface{}) (res *beefyclienttypes.PartialMMRLeaf) {
+func parsePartialMMRLeaf(i any) (res *beefyclienttypes.PartialMMRLeaf) {
 	attrs := i.(ibcEventQueryItem)
 
 	res = &beefyclienttypes.PartialMMRLeaf{
@@ -416,7 +483,7 @@ func parsePartialMMRLeaf(i interface{}) (res *beefyclienttypes.PartialMMRLeaf) {
 }
 
 // parses signed commitment and corresposing signatures
-func parseSignedCommitment(i interface{}) (res *beefyclienttypes.SignedCommitment) {
+func parseSignedCommitment(i any) (res *beefyclienttypes.SignedCommitment) {
 	attrs := i.(ibcEventQueryItem)
 
 	res = &beefyclienttypes.SignedCommitment{
@@ -428,7 +495,7 @@ func parseSignedCommitment(i interface{}) (res *beefyclienttypes.SignedCommitmen
 }
 
 // parses the commitment and payload attributes
-func parseCommitment(i interface{}) (res *beefyclienttypes.Commitment) {
+func parseCommitment(i any) (res *beefyclienttypes.Commitment) {
 	attrs := i.(ibcEventQueryItem)
 
 	res = &beefyclienttypes.Commitment{
@@ -441,7 +508,7 @@ func parseCommitment(i interface{}) (res *beefyclienttypes.Commitment) {
 }
 
 // parses the payload id and attributes
-func parsePayload(i interface{}) (res []*beefyclienttypes.Payload) {
+func parsePayload(i any) (res []*beefyclienttypes.Payload) {
 	sliceAttrs := i.(rpcclienttypes.IBCEventsQueryResult)
 	res = []*beefyclienttypes.Payload{}
 
@@ -459,7 +526,7 @@ func parsePayload(i interface{}) (res []*beefyclienttypes.Payload) {
 }
 
 // parses the commitment signatures attributes
-func parseSignatures(i interface{}) (res []*beefyclienttypes.CommitmentSignature) {
+func parseSignatures(i any) (res []*beefyclienttypes.CommitmentSignature) {
 	sliceAttrs := i.(rpcclienttypes.IBCEventsQueryResult)
 	res = []*beefyclienttypes.CommitmentSignature{}
 
@@ -477,7 +544,7 @@ func parseSignatures(i interface{}) (res []*beefyclienttypes.CommitmentSignature
 }
 
 // parses mmr leaf attributes of beefy
-func parseBeefyMMRLeaf(i interface{}) (res *beefyclienttypes.BeefyMMRLeaf) {
+func parseBeefyMMRLeaf(i any) (res *beefyclienttypes.BeefyMMRLeaf) {
 	attrs := i.(ibcEventQueryItem)
 
 	res = &beefyclienttypes.BeefyMMRLeaf{
@@ -492,7 +559,7 @@ func parseBeefyMMRLeaf(i interface{}) (res *beefyclienttypes.BeefyMMRLeaf) {
 }
 
 // parses authority set attributes
-func parseAuthoritySet(i interface{}) (res beefyclienttypes.BeefyAuthoritySet) {
+func parseAuthoritySet(i any) (res beefyclienttypes.BeefyAuthoritySet) {
 	attrs := i.(ibcEventQueryItem)
 
 	res = beefyclienttypes.BeefyAuthoritySet{
diff --git a/relayer/chains/substrate/substrate_chain_processor.go b/relayer/chains/substrate/substrate_chain_processor.go
index 1c9a924..fc82f3a 100644
--- a/relayer/chains/substrate/substrate_chain_processor.go
+++ b/relayer/chains/substrate/substrate_chain_processor.go
@@ -328,7 +328,7 @@ func (scp *SubstrateChainProcessor) queryCycle(ctx context.Context, persistence
 		ibcHeaderCache[heightUint64] = latestHeader
 		ppChanged = true
 
-		messages := scp.ibcMessagesFromEvents(ibcEvents, heightUint64)
+		messages := scp.ibcMessagesFromEvents(ibcEvents)
 
 		for _, m := range messages {
 			scp.handleMessage(m, ibcMessagesCache)


Test patch:
diff --git a/relayer/chains/substrate/event_parser_test.go b/relayer/chains/substrate/event_parser_test.go
new file mode 100644
index 0000000..e06c9a4
--- /dev/null
+++ b/relayer/chains/substrate/event_parser_test.go
@@ -0,0 +1,325 @@
+package substrate
+
+import (
+	"encoding/hex"
+	"encoding/json"
+	"testing"
+
+	rpcclienttypes "github.com/ComposableFi/go-substrate-rpc-client/v4/types"
+	clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types"
+	chantypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types"
+	"github.com/cosmos/relayer/v2/relayer/provider"
+	"github.com/google/go-cmp/cmp"
+	"github.com/spf13/cast"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/zap"
+)
+
+const (
+	testSequence       = uint64(1)
+	testConnectionID1  = "connection-0"
+	testConnectionID2  = "connection-1"
+	testClientID1      = "client-0"
+	testClientID2      = "client-1"
+	testChannelID1     = "channel-0"
+	testChannelID2     = "channel-1"
+	testPortID1        = "port-0"
+	testPortID2        = "port-1"
+	testRevisionNumber = 1
+	testRevisionHeight = 10
+	testTimestamp      = "2022-10-06T11:00:02.664464Z"
+	testClientType     = uint32(1)
+	testPacketDataHex  = "68656C6C6F"
+	testPacketAckHex   = "68656C6C6F"
+)
+
+func TestParsePacket(t *testing.T) {
+
+	packetStr := `{
+		"height": {
+			"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+			"revision_height": ` + cast.ToString(testRevisionHeight) + `
+		},
+		"sequence": ` + cast.ToString(testSequence) + `,
+		"source_port": "` + testPortID1 + `",
+		"source_channel": "` + testChannelID1 + `",
+		"destination_port": "` + testPortID2 + `",
+		"destination_channel": "` + testChannelID2 + `",
+		"data": "` + testPacketDataHex + `",
+		"timeout_height": {
+			"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+			"revision_height": ` + cast.ToString(testRevisionHeight) + `
+		},
+		"timeout_timestamp": {
+			"time": "` + testTimestamp + `"
+		},
+		"ack": "` + testPacketAckHex + `"
+	}`
+
+	var packetEventAttributes ibcEventQueryItem
+	err := json.Unmarshal([]byte(packetStr), &packetEventAttributes)
+	require.NoError(t, err)
+
+	parsed := new(packetInfo)
+	parsed.parseAttrs(zap.NewNop(), packetEventAttributes)
+
+	packetData, err := hex.DecodeString(testPacketDataHex)
+	require.NoError(t, err, "error decoding test packet data")
+
+	ackData, err := hex.DecodeString(testPacketAckHex)
+	require.NoError(t, err, "error decoding test ack data")
+
+	require.Empty(t, cmp.Diff(provider.PacketInfo(*parsed), provider.PacketInfo{
+		Height:   testRevisionHeight,
+		Sequence: testSequence,
+		Data:     packetData,
+		TimeoutHeight: clienttypes.Height{
+			RevisionNumber: testRevisionNumber,
+			RevisionHeight: testRevisionHeight,
+		},
+		TimeoutTimestamp: uint64(1665054002664464000),
+		SourceChannel:    testChannelID1,
+		SourcePort:       testPortID1,
+		DestChannel:      testChannelID2,
+		DestPort:         testPortID2,
+		Ack:              ackData,
+	}), "parsed does not match expected")
+}
+
+func TestParseClient(t *testing.T) {
+
+	clientStr := `{
+		"height": {
+			"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+			"revision_height": ` + cast.ToString(testRevisionHeight) + `
+		},
+		"client_id": "` + testClientID1 + `",
+		"consensus_height": {
+			"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+			"revision_height": ` + cast.ToString(testRevisionHeight) + `
+		},
+		"client_type": "` + cast.ToString(testClientType) + `"
+	}`
+
+	var clientEventAttributes ibcEventQueryItem
+	err := json.Unmarshal([]byte(clientStr), &clientEventAttributes)
+	require.NoError(t, err)
+
+	parsed := new(clientInfo)
+	parsed.parseAttrs(zap.NewNop(), clientEventAttributes)
+
+	require.Empty(t, cmp.Diff(*parsed, clientInfo{
+		height: clienttypes.Height{
+			RevisionNumber: testRevisionNumber,
+			RevisionHeight: testRevisionHeight,
+		},
+		clientID: testClientID1,
+		consensusHeight: clienttypes.Height{
+			RevisionNumber: testRevisionNumber,
+			RevisionHeight: testRevisionHeight,
+		},
+		clientType: testClientType,
+	}, cmp.AllowUnexported(clientInfo{}, clienttypes.Height{})), "parsed client info does not match expected")
+}
+
+func TestParseClientUpdate(t *testing.T) {
+
+	// header is always ignored, so there is no need to populate it
+	clientStr := `{
+		"common": {
+			"height": {
+				"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+				"revision_height": ` + cast.ToString(testRevisionHeight) + `
+			},
+			"client_id": "` + testClientID1 + `",
+			"consensus_height": {
+				"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+				"revision_height": ` + cast.ToString(testRevisionHeight) + `
+			},
+			"client_type": "` + cast.ToString(testClientType) + `"
+		}
+	}`
+
+	var clientEventAttributes ibcEventQueryItem
+	err := json.Unmarshal([]byte(clientStr), &clientEventAttributes)
+	require.NoError(t, err)
+
+	parsed := new(clientUpdateInfo)
+	parsed.parseAttrs(zap.NewNop(), clientEventAttributes)
+
+	require.Empty(t, cmp.Diff(*parsed, clientUpdateInfo{
+		common: clientInfo{
+			height: clienttypes.Height{
+				RevisionNumber: testRevisionNumber,
+				RevisionHeight: testRevisionHeight,
+			},
+			clientID: testClientID1,
+			consensusHeight: clienttypes.Height{
+				RevisionNumber: testRevisionNumber,
+				RevisionHeight: testRevisionHeight,
+			},
+			clientType: testClientType,
+		},
+	}, cmp.AllowUnexported(clientUpdateInfo{}, clientInfo{}, clienttypes.Height{})), "parsed client info does not match expected")
+}
+
+func TestParseChannel(t *testing.T) {
+
+	clientStr := `{
+		"height": {
+			"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+			"revision_height": ` + cast.ToString(testRevisionHeight) + `
+		},
+		"port_id": "` + testPortID1 + `",
+		"channel_id": "` + testChannelID1 + `",
+		"connection_id": "` + testConnectionID1 + `",
+		"counterparty_port_id": "` + testPortID2 + `",
+		"counterparty_channel_id": "` + testChannelID2 + `"
+	}`
+
+	var clientAttributes ibcEventQueryItem
+	err := json.Unmarshal([]byte(clientStr), &clientAttributes)
+	require.NoError(t, err)
+
+	parsed := new(channelInfo)
+	parsed.parseAttrs(zap.NewNop(), clientAttributes)
+
+	require.Empty(t, cmp.Diff(provider.ChannelInfo(*parsed), provider.ChannelInfo{
+		Height:                testRevisionHeight,
+		ConnID:                testConnectionID1,
+		ChannelID:             testChannelID1,
+		PortID:                testPortID1,
+		CounterpartyChannelID: testChannelID2,
+		CounterpartyPortID:    testPortID2,
+	}), "parsed channel info does not match expected")
+}
+
+func TestParseConnection(t *testing.T) {
+
+	connectionStr := `{
+		"height": {
+			"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+			"revision_height": ` + cast.ToString(testRevisionHeight) + `
+		},
+		"connection_id": "` + testConnectionID1 + `",
+		"client_id": "` + testClientID1 + `",
+		"counterparty_connection_id": "` + testConnectionID2 + `",
+		"counterparty_client_id": "` + testClientID2 + `"
+	}`
+
+	var connectionAttributes ibcEventQueryItem
+	err := json.Unmarshal([]byte(connectionStr), &connectionAttributes)
+	require.NoError(t, err)
+
+	parsed := new(connectionInfo)
+	parsed.parseAttrs(zap.NewNop(), connectionAttributes)
+
+	require.Empty(t, cmp.Diff(provider.ConnectionInfo(*parsed), provider.ConnectionInfo{
+		Height:               testRevisionHeight,
+		ClientID:             testClientID1,
+		ConnID:               testConnectionID1,
+		CounterpartyClientID: testClientID2,
+		CounterpartyConnID:   testConnectionID2,
+	}), "parsed connection info does not match expected")
+}
+
+func TestParseEvents(t *testing.T) {
+	events := `[
+		{
+			"` + UpdateClient + `": {
+				"common": {
+					"height": {
+						"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+						"revision_height": ` + cast.ToString(testRevisionHeight) + `
+					},
+					"client_id": "` + testClientID1 + `",
+					"consensus_height": {
+						"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+						"revision_height": ` + cast.ToString(testRevisionHeight) + `
+					},
+					"client_type": "` + cast.ToString(testClientType) + `"
+				}
+			}
+		},
+		{
+			"` + ReceivePacket + `": {
+				"height": {
+					"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+					"revision_height": ` + cast.ToString(testRevisionHeight) + `
+				},
+				"sequence": ` + cast.ToString(testSequence) + `,
+				"source_port": "` + testPortID1 + `",
+				"source_channel": "` + testChannelID1 + `",
+				"destination_port": "` + testPortID2 + `",
+				"destination_channel": "` + testChannelID2 + `",
+				"data": "` + testPacketDataHex + `",
+				"timeout_height": {
+					"revision_number": ` + cast.ToString(testRevisionNumber) + `,
+					"revision_height": ` + cast.ToString(testRevisionHeight) + `
+				},
+				"timeout_timestamp": {
+					"time": "` + testTimestamp + `"
+				},
+				"ack": "` + testPacketAckHex + `"
+			}
+		}
+	]`
+
+	var eventsResult rpcclienttypes.IBCEventsQueryResult
+	err := json.Unmarshal([]byte(events), &eventsResult)
+	require.NoError(t, err)
+
+	proc := SubstrateChainProcessor{}
+	ibcMessages := proc.ibcMessagesFromEvents(eventsResult)
+
+	require.Len(t, ibcMessages, 2)
+
+	msgUpdateClient := ibcMessages[0]
+	require.Equal(t, clienttypes.EventTypeUpdateClient, msgUpdateClient.eventType)
+
+	clientInfoParsed, isClientInfo := msgUpdateClient.info.(*clientUpdateInfo)
+	require.True(t, isClientInfo, "messageInfo is not clientInfo")
+
+	require.Empty(t, cmp.Diff(*clientInfoParsed, clientUpdateInfo{
+		common: clientInfo{
+			height: clienttypes.Height{
+				RevisionNumber: testRevisionNumber,
+				RevisionHeight: testRevisionHeight,
+			},
+			clientID: testClientID1,
+			consensusHeight: clienttypes.Height{
+				RevisionNumber: testRevisionNumber,
+				RevisionHeight: testRevisionHeight,
+			},
+			clientType: testClientType,
+		},
+	}, cmp.AllowUnexported(clientUpdateInfo{}, clientInfo{}, clienttypes.Height{})), "parsed client info does not match expected")
+
+	msgRecvPacket := ibcMessages[1]
+	require.Equal(t, chantypes.EventTypeRecvPacket, msgRecvPacket.eventType, "message event is not recv_packet")
+
+	packetInfoParsed, isPacketInfo := msgRecvPacket.info.(*packetInfo)
+	require.True(t, isPacketInfo, "messageInfo is not packetInfo")
+
+	packetAck, err := hex.DecodeString(testPacketAckHex)
+	require.NoError(t, err, "error decoding test packet ack")
+
+	packetData, err := hex.DecodeString(testPacketDataHex)
+	require.NoError(t, err, "error decoding test packet data")
+
+	require.Empty(t, cmp.Diff(provider.PacketInfo(*packetInfoParsed), provider.PacketInfo{
+		Height:   testRevisionHeight,
+		Sequence: testSequence,
+		Data:     packetData,
+		TimeoutHeight: clienttypes.Height{
+			RevisionNumber: testRevisionNumber,
+			RevisionHeight: testRevisionHeight,
+		},
+		TimeoutTimestamp: uint64(cast.ToTime(testTimestamp).UnixNano()),
+		SourceChannel:    testChannelID1,
+		SourcePort:       testPortID1,
+		DestChannel:      testChannelID2,
+		DestPort:         testPortID2,
+		Ack:              packetAck,
+	}), "parsed packet info does not match expected")
+}