Repository: cosmos/relayer
Pull request #1247 (2023-07-26 20:49:47)

Add extra client info when querying client expiration
This PR reformats the `rly q clients-expiration` command.

Specifically it adds this info to the query:
 - last update height (revision height) 
 - client trusting period

This extra info can be useful when troubleshooting expired light clients

Example:

![Screenshot 2023-07-26 at 1 47 28 PM](https://github.com/cosmos/relayer/assets/56059752/fc84625e-5964-4577-9c3f-b730665b537f)


Also, this command can now be run if only one client exists in a given path. 
This information can be useful when substituting new clients with expired clients.


Patch:
diff --git a/cmd/query.go b/cmd/query.go
index cd91bc2..15ccd74 100644
--- a/cmd/query.go
+++ b/cmd/query.go
@@ -1070,9 +1070,10 @@ $ %s query unrelayed-acks demo-path channel-0`,
 
 func queryClientsExpiration(a *appState) *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "clients-expiration path",
-		Short: "query for light clients expiration date",
-		Args:  withUsage(cobra.ExactArgs(1)),
+		Use:     "clients-expiration path",
+		Aliases: []string{"ce"},
+		Short:   "query for light clients expiration date",
+		Args:    withUsage(cobra.ExactArgs(1)),
 		Example: strings.TrimSpace(fmt.Sprintf(`
 $ %s query clients-expiration demo-path`,
 			appName,
@@ -1095,17 +1096,29 @@ $ %s query clients-expiration demo-path`,
 				return err
 			}
 
-			srcExpiration, err := relayer.QueryClientExpiration(cmd.Context(), c[src], c[dst])
-			if err != nil {
-				return err
+			srcExpiration, srcClientInfo, errSrc := relayer.QueryClientExpiration(cmd.Context(), c[src], c[dst])
+			if errSrc != nil && !strings.Contains(errSrc.Error(), "light client not found") {
+				return errSrc
 			}
-			dstExpiration, err := relayer.QueryClientExpiration(cmd.Context(), c[dst], c[src])
-			if err != nil {
-				return err
+			dstExpiration, dstClientInfo, errDst := relayer.QueryClientExpiration(cmd.Context(), c[dst], c[src])
+			if errDst != nil && !strings.Contains(errDst.Error(), "light client not found") {
+				return errDst
+			}
+
+			// if only the src light client is found, just print info for source light client
+			if errSrc == nil && errDst != nil {
+				fmt.Fprintln(cmd.OutOrStdout(), relayer.SPrintClientExpiration(c[src], srcExpiration, srcClientInfo))
+				return nil
+			}
+
+			// if only the dst light client is found, just print info for destination light client
+			if errDst == nil && errSrc != nil {
+				fmt.Fprintln(cmd.OutOrStdout(), relayer.SPrintClientExpiration(c[dst], dstExpiration, dstClientInfo))
+				return nil
 			}
 
-			fmt.Fprintf(cmd.OutOrStdout(), relayer.SPrintClientExpiration(c[src], srcExpiration))
-			fmt.Fprintf(cmd.OutOrStdout(), relayer.SPrintClientExpiration(c[dst], dstExpiration))
+			fmt.Fprintln(cmd.OutOrStdout(), relayer.SPrintClientExpiration(c[src], srcExpiration, srcClientInfo))
+			fmt.Fprintln(cmd.OutOrStdout(), relayer.SPrintClientExpiration(c[dst], dstExpiration, dstClientInfo))
 
 			return nil
 		},
diff --git a/relayer/query.go b/relayer/query.go
index 24c4067..08b2ec1 100644
--- a/relayer/query.go
+++ b/relayer/query.go
@@ -272,39 +272,48 @@ func QueryBalance(ctx context.Context, chain *Chain, address string, showDenoms
 	return out, nil
 }
 
-func QueryClientExpiration(ctx context.Context, src, dst *Chain) (time.Time, error) {
+func QueryClientExpiration(ctx context.Context, src, dst *Chain) (time.Time, ClientStateInfo, error) {
 	latestHeight, err := src.ChainProvider.QueryLatestHeight(ctx)
 	if err != nil {
-		return time.Time{}, err
+		return time.Time{}, ClientStateInfo{}, err
 	}
 
 	clientStateRes, err := src.ChainProvider.QueryClientStateResponse(ctx, latestHeight, src.ClientID())
 	if err != nil {
-		return time.Time{}, err
+		return time.Time{}, ClientStateInfo{}, err
 	}
 
 	clientInfo, err := ClientInfoFromClientState(clientStateRes.ClientState)
 	if err != nil {
-		return time.Time{}, err
+		return time.Time{}, ClientStateInfo{}, err
 	}
 
 	clientTime, err := dst.ChainProvider.BlockTime(ctx, int64(clientInfo.LatestHeight.GetRevisionHeight()))
 	if err != nil {
-		return time.Time{}, err
+		return time.Time{}, ClientStateInfo{}, err
 	}
 
-	return clientTime.Add(clientInfo.TrustingPeriod), nil
+	return clientTime.Add(clientInfo.TrustingPeriod), clientInfo, nil
 }
 
-func SPrintClientExpiration(chain *Chain, expiration time.Time) string {
+func SPrintClientExpiration(chain *Chain, expiration time.Time, clientInfo ClientStateInfo) string {
 	now := time.Now()
 	remainingTime := expiration.Sub(now)
 	expirationFormatted := expiration.Format(time.RFC822)
 
-	if remainingTime < 0 {
-		return fmt.Sprintf("client %s (%s) is already expired (%s)\n",
-			chain.ClientID(), chain.ChainID(), expirationFormatted)
+	var status string
+	if remainingTime <= 0 {
+		status = "EXPIRED"
+	} else {
+		status = "GOOD"
 	}
-	return fmt.Sprintf("client %s (%s) expires in %s (%s)\n",
-		chain.ClientID(), chain.ChainID(), remainingTime.Round(time.Second), expirationFormatted)
+
+	return fmt.Sprintf(`
+	client: %s (%s)
+		HEALTH:              %s
+		TIME:                %s (%s)
+		LAST UPDATE HEIGHT:  %d
+		TRUSTING PERIOD:     %s
+	`,
+		chain.ClientID(), chain.ChainID(), status, expirationFormatted, remainingTime.Round(time.Second), clientInfo.LatestHeight.GetRevisionHeight(), clientInfo.TrustingPeriod.String())
 }


Test patch:
diff --git a/relayer/query_test.go b/relayer/query_test.go
index fb67c60..39dc837 100644
--- a/relayer/query_test.go
+++ b/relayer/query_test.go
@@ -1,76 +1,124 @@
 package relayer
 
 import (
-	"github.com/cosmos/relayer/v2/relayer/chains/cosmos"
 	"testing"
 	"time"
 
+	"github.com/cosmos/relayer/v2/relayer/chains/cosmos"
+
+	clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types"
+	ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported"
 	"github.com/stretchr/testify/require"
 )
 
 func TestSPrintClientExpiration_PrintChainId(t *testing.T) {
 	previousTime := time.Now().Add(10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
 
 	chain := mockChain("expected-chain-id", "test-client-id")
-	expiration := SPrintClientExpiration(chain, previousTime)
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, previousTime, *clientStateInfo)
 
 	require.Contains(t, expiration, "expected-chain-id")
 }
 
 func TestSPrintClientExpiration_PrintClientId(t *testing.T) {
 	previousTime := time.Now().Add(10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
 
 	chain := mockChain("test-chain-id", "expected-client-id")
-	expiration := SPrintClientExpiration(chain, previousTime)
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, previousTime, *clientStateInfo)
 
 	require.Contains(t, expiration, "expected-client-id")
 }
 
-func TestSPrintClientExpiration_PrintIsAlreadyExpired_WhenTimeIsInPast(t *testing.T) {
+func TestSPrintClientExpiration_PrintExpired_WhenTimeIsInPast(t *testing.T) {
 	previousTime := time.Now().Add(-10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
 
 	chain := mockChain("test-chain-id", "test-client-id")
-	expiration := SPrintClientExpiration(chain, previousTime)
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, previousTime, *clientStateInfo)
 
-	require.Contains(t, expiration, "is already expired")
+	require.Contains(t, expiration, "EXPIRED")
 }
 
 func TestSPrintClientExpiration_PrintRFC822FormattedTime_WhenTimeIsInPast(t *testing.T) {
 	pastTime := time.Now().Add(-10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
 
-	chain := mockChain("test-chain-id", "test-client-id")
-	expiration := SPrintClientExpiration(chain, pastTime)
+	chain := mockChain("expected-chain-id", "test-client-id")
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, pastTime, *clientStateInfo)
 
 	require.Contains(t, expiration, pastTime.Format(time.RFC822))
 }
 
-func TestSPrintClientExpiration_PrintExpiresIn_WhenTimeIsInFuture(t *testing.T) {
+func TestSPrintClientExpiration_PrintGood_WhenTimeIsInFuture(t *testing.T) {
 	previousTime := time.Now().Add(10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
 
 	chain := mockChain("test-chain-id", "test-client-id")
-	expiration := SPrintClientExpiration(chain, previousTime)
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, previousTime, *clientStateInfo)
 
-	require.Contains(t, expiration, "expires in")
+	require.Contains(t, expiration, "GOOD")
 }
 
 func TestSPrintClientExpiration_PrintRFC822FormattedTime_WhenTimeIsInFuture(t *testing.T) {
 	futureTime := time.Now().Add(10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
 
 	chain := mockChain("test-chain-id", "test-client-id")
-	expiration := SPrintClientExpiration(chain, futureTime)
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, futureTime, *clientStateInfo)
 
 	require.Contains(t, expiration, futureTime.Format(time.RFC822))
 }
 
 func TestSPrintClientExpiration_PrintRemainingTime_WhenTimeIsInFuture(t *testing.T) {
 	futureTime := time.Now().Add(10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
 
 	chain := mockChain("test-chain-id", "test-client-id")
-	expiration := SPrintClientExpiration(chain, futureTime)
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, futureTime, *clientStateInfo)
 
 	require.Contains(t, expiration, "10h0m0s")
 }
 
+func TestSPrintClientExpiration_TrustingPeriod(t *testing.T) {
+	previousTime := time.Now().Add(10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
+
+	chain := mockChain("expected-chain-id", "test-client-id")
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, previousTime, *clientStateInfo)
+
+	require.Contains(t, expiration, "1h0m0s")
+}
+
+func TestSPrintClientExpiration_LastUpdateHeight(t *testing.T) {
+	previousTime := time.Now().Add(10 * time.Hour)
+	mockHeight := clienttypes.NewHeight(1, 100)
+	trustingPeriod := time.Duration(1 * time.Hour)
+
+	chain := mockChain("expected-chain-id", "test-client-id")
+	clientStateInfo := mockClientStateInfo("test-chain-id", trustingPeriod, mockHeight)
+	expiration := SPrintClientExpiration(chain, previousTime, *clientStateInfo)
+
+	require.Contains(t, expiration, "100")
+}
+
 func mockChain(chainId string, clientId string) *Chain {
 	return &Chain{
 		Chainid: chainId,
@@ -85,3 +133,12 @@ func mockChain(chainId string, clientId string) *Chain {
 		},
 	}
 }
+
+func mockClientStateInfo(chainID string, trustingPeriod time.Duration, latestHeight ibcexported.Height) *ClientStateInfo {
+	mockHeight := clienttypes.NewHeight(1, 100)
+	return &ClientStateInfo{
+		ChainID:        chainID,
+		TrustingPeriod: time.Duration(1 * time.Hour),
+		LatestHeight:   mockHeight,
+	}
+}