Repository: hyperlane-xyz/hyperlane-monorepo
Pull request #6124: chore: add cosmos native signer strategy to cli
Author: login: troykessler
Created: 2025-05-05T08:57:05Z | Merged: 2025-05-13T15:32:12Z

### Description

This PR implements and adds the cosmos native signer strategy to the cli context so users can also sign and deploy to cosmos native chains. Following PRs will use this to support the goal of having full cosmos native support in the hyperlane CLI.

Note that this is the second PR of https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/6050 which is being split up in order to make it easier to review

### Drive-by changes

-

### Related issues

-

### Backward compatibility

Yes

### Testing

Manual testing with local cosmos chain



Diff:
diff --git a/typescript/cli/package.json b/typescript/cli/package.json
index 24e13a6e588..b91078a44c7 100644
--- a/typescript/cli/package.json
+++ b/typescript/cli/package.json
@@ -5,9 +5,11 @@
   "devDependencies": {
     "@aws-sdk/client-kms": "^3.577.0",
     "@aws-sdk/client-s3": "^3.577.0",
+    "@cosmjs/stargate": "^0.32.4",
     "@eslint/js": "^9.15.0",
     "@ethersproject/abi": "*",
     "@ethersproject/providers": "*",
+    "@hyperlane-xyz/cosmos-sdk": "12.5.0",
     "@hyperlane-xyz/registry": "15.0.0",
     "@hyperlane-xyz/sdk": "12.5.0",
     "@hyperlane-xyz/utils": "12.5.0",
diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts
index cb2abaf437a..64110fbe40d 100644
--- a/typescript/cli/src/config/strategy.ts
+++ b/typescript/cli/src/config/strategy.ts
@@ -10,15 +10,17 @@ import {
 import {
   ProtocolType,
   assert,
+  errorToString,
   isAddress,
   isPrivateKeyEvm,
 } from '@hyperlane-xyz/utils';
 
 import { CommandContext } from '../context/types.js';
-import { errorRed, log, logBlue, logGreen } from '../logger.js';
+import { errorRed, log, logBlue, logGreen, logRed } from '../logger.js';
 import { runSingleChainSelectionStep } from '../utils/chains.js';
 import {
   indentYamlOrJson,
+  isFile,
   readYamlOrJson,
   writeYamlOrJson,
 } from '../utils/files.js';
@@ -36,6 +38,28 @@ export async function readChainSubmissionStrategyConfig(
   return parseResult;
 }
 
+/**
+ * Safely reads chain submission strategy config, returns empty object if any errors occur
+ */
+export async function safeReadChainSubmissionStrategyConfig(
+  filePath: string,
+): Promise<ChainSubmissionStrategy> {
+  try {
+    const trimmedFilePath = filePath.trim();
+    if (!isFile(trimmedFilePath)) {
+      logBlue(`File ${trimmedFilePath} does not exist, returning empty config`);
+      return {};
+    }
+    return await readChainSubmissionStrategyConfig(trimmedFilePath);
+  } catch (error) {
+    logRed(
+      `Failed to read strategy config, defaulting to empty config:`,
+      errorToString(error),
+    );
+    return {};
+  }
+}
+
 export async function createStrategyConfig({
   context,
   outPath,
diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts
index 941bd76d37a..e46e2fdcc4a 100644
--- a/typescript/cli/src/context/context.ts
+++ b/typescript/cli/src/context/context.ts
@@ -7,12 +7,14 @@ import {
   ChainMap,
   ChainMetadata,
   ChainName,
+  MultiProtocolProvider,
   MultiProvider,
 } from '@hyperlane-xyz/sdk';
 import { isNullish, rootLogger } from '@hyperlane-xyz/utils';
 
+import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js';
 import { isSignCommand } from '../commands/signCommands.js';
-import { readChainSubmissionStrategyConfig } from '../config/strategy.js';
+import { safeReadChainSubmissionStrategyConfig } from '../config/strategy.js';
 import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js';
 import { logBlue } from '../logger.js';
 import { runSingleChainSelectionStep } from '../utils/chains.js';
@@ -54,13 +56,15 @@ export async function contextMiddleware(argv: Record<string, any>) {
 }
 
 export async function signerMiddleware(argv: Record<string, any>) {
-  const { key, requiresKey, multiProvider, strategyPath } = argv.context;
+  const { key, requiresKey, multiProvider, strategyPath, chainMetadata } =
+    argv.context;
 
+  const multiProtocolProvider = new MultiProtocolProvider(chainMetadata);
   if (!requiresKey) return argv;
 
-  const strategyConfig = strategyPath
-    ? await readChainSubmissionStrategyConfig(strategyPath)
-    : {};
+  const strategyConfig = await safeReadChainSubmissionStrategyConfig(
+    strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH,
+  );
 
   /**
    * Intercepts Hyperlane command to determine chains.
@@ -79,6 +83,7 @@ export async function signerMiddleware(argv: Record<string, any>) {
     strategyConfig,
     chains,
     multiProvider,
+    multiProtocolProvider,
     { key },
   );
 
@@ -86,7 +91,8 @@ export async function signerMiddleware(argv: Record<string, any>) {
    * @notice Attaches signers to MultiProvider and assigns it to argv.multiProvider
    */
   argv.multiProvider = await multiProtocolSigner.getMultiProvider();
-  argv.multiProtocolSigner = multiProtocolSigner;
+  argv.multiProtocolProvider = multiProtocolProvider;
+  argv.context.multiProtocolSigner = multiProtocolSigner;
 
   return argv;
 }
@@ -120,12 +126,14 @@ export async function getContext({
   }
 
   const multiProvider = await getMultiProvider(registry);
+  const multiProtocolProvider = await getMultiProtocolProvider(registry);
 
   return {
     registry,
     requiresKey,
     chainMetadata: multiProvider.metadata,
     multiProvider,
+    multiProtocolProvider,
     key,
     skipConfirmation: !!skipConfirmation,
     signerAddress,
@@ -168,6 +176,7 @@ export async function getDryRunContext(
   await verifyAnvil();
 
   let multiProvider = await getMultiProvider(registry);
+  const multiProtocolProvider = await getMultiProtocolProvider(registry);
   multiProvider = await forkNetworkToMultiProvider(multiProvider, chain);
   const { impersonatedKey, impersonatedSigner } = await getImpersonatedSigner({
     fromAddress,
@@ -182,6 +191,7 @@ export async function getDryRunContext(
     key: impersonatedKey,
     signer: impersonatedSigner,
     multiProvider: multiProvider,
+    multiProtocolProvider: multiProtocolProvider,
     skipConfirmation: !!skipConfirmation,
     isDryRun: true,
     dryRunChain: chain,
@@ -200,6 +210,11 @@ async function getMultiProvider(registry: IRegistry, signer?: ethers.Signer) {
   return multiProvider;
 }
 
+async function getMultiProtocolProvider(registry: IRegistry) {
+  const chainMetadata = await registry.getMetadata();
+  return new MultiProtocolProvider(chainMetadata);
+}
+
 /**
  * Requests and saves Block Explorer API keys for the specified chains, prompting the user if necessary.
  *
diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts
index b91242b42df..e6257e5ee7b 100644
--- a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts
+++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts
@@ -1,8 +1,11 @@
 import { Signer } from 'ethers';
 
+import { SigningHyperlaneModuleClient } from '@hyperlane-xyz/cosmos-sdk';
 import { ChainName, ChainSubmissionStrategy } from '@hyperlane-xyz/sdk';
 import { Address } from '@hyperlane-xyz/utils';
 
+export type TypedSigner = Signer | SigningHyperlaneModuleClient;
+
 export interface SignerConfig {
   privateKey: string;
   address?: Address; // For chains like StarkNet that require address
@@ -11,12 +14,12 @@ export interface SignerConfig {
 
 export interface IMultiProtocolSigner {
   getSignerConfig(chain: ChainName): Promise<SignerConfig> | SignerConfig;
-  getSigner(config: SignerConfig): Signer;
+  getSigner(config: SignerConfig): Promise<TypedSigner>;
 }
 
 export abstract class BaseMultiProtocolSigner implements IMultiProtocolSigner {
   constructor(protected config: ChainSubmissionStrategy) {}
 
   abstract getSignerConfig(chain: ChainName): Promise<SignerConfig>;
-  abstract getSigner(config: SignerConfig): Signer;
+  abstract getSigner(config: SignerConfig): Promise<TypedSigner>;
 }
diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
index d6f83572fb6..45c5cfc8fcd 100644
--- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
+++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
@@ -1,7 +1,13 @@
+import {
+  DirectSecp256k1HdWallet,
+  DirectSecp256k1Wallet,
+} from '@cosmjs/proto-signing';
+import { GasPrice } from '@cosmjs/stargate';
 import { password } from '@inquirer/prompts';
-import { Signer, Wallet } from 'ethers';
+import { Signer, Wallet, ethers } from 'ethers';
 import { Wallet as ZKSyncWallet } from 'zksync-ethers';
 
+import { SigningHyperlaneModuleClient } from '@hyperlane-xyz/cosmos-sdk';
 import {
   ChainName,
   ChainSubmissionStrategy,
@@ -9,7 +15,7 @@ import {
   MultiProvider,
   TxSubmitterType,
 } from '@hyperlane-xyz/sdk';
-import { ProtocolType } from '@hyperlane-xyz/utils';
+import { ProtocolType, assert, ensure0x } from '@hyperlane-xyz/utils';
 
 import {
   BaseMultiProtocolSigner,
@@ -30,6 +36,8 @@ export class MultiProtocolSignerFactory {
         if (technicalStack === ChainTechnicalStack.ZkSync)
           return new ZKSyncSignerStrategy(strategyConfig);
         return new EthereumSignerStrategy(strategyConfig);
+      case ProtocolType.CosmosNative:
+        return new CosmosNativeSignerStrategy(strategyConfig);
       default:
         throw new Error(`Unsupported protocol: ${protocol}`);
     }
@@ -52,7 +60,7 @@ class EthereumSignerStrategy extends BaseMultiProtocolSigner {
     return { privateKey };
   }
 
-  getSigner(config: SignerConfig): Signer {
+  async getSigner(config: SignerConfig): Promise<Signer> {
     return new Wallet(config.privateKey);
   }
 }
@@ -73,7 +81,59 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner {
     return { privateKey };
   }
 
-  getSigner(config: SignerConfig): Signer {
+  async getSigner(config: SignerConfig): Promise<Signer> {
     return new ZKSyncWallet(config.privateKey);
   }
 }
+
+class CosmosNativeSignerStrategy extends BaseMultiProtocolSigner {
+  async getSignerConfig(chain: ChainName): Promise<SignerConfig> {
+    const submitter = this.config[chain]?.submitter as {
+      privateKey?: string;
+    };
+
+    const privateKey =
+      submitter?.privateKey ??
+      (await password({
+        message: `Please enter the private key for chain ${chain}`,
+      }));
+
+    return {
+      privateKey,
+    };
+  }
+
+  async getSigner({
+    privateKey,
+    extraParams,
+  }: SignerConfig): Promise<SigningHyperlaneModuleClient> {
+    assert(
+      extraParams?.provider && extraParams?.prefix && extraParams?.gasPrice,
+      'Missing Cosmos Signer arguments',
+    );
+
+    let wallet;
+
+    if (ethers.utils.isHexString(ensure0x(privateKey))) {
+      wallet = await DirectSecp256k1Wallet.fromKey(
+        Buffer.from(privateKey, 'hex'),
+        extraParams.prefix,
+      );
+    } else {
+      wallet = await DirectSecp256k1HdWallet.fromMnemonic(privateKey, {
+        prefix: extraParams.prefix,
+      });
+    }
+
+    const cometClient = extraParams?.provider.getCometClient();
+
+    // parse gas price so it has the correct format
+    const gasPrice = GasPrice.fromString(
+      `${extraParams.gasPrice.amount}${extraParams.gasPrice.denom}`,
+    );
+
+    return SigningHyperlaneModuleClient.createWithSigner(cometClient, wallet, {
+      gasPrice,
+    });
+  }
+}
diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts
index 394e0b1391b..097333e56a7 100644
--- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts
+++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts
@@ -1,16 +1,22 @@
 import { Signer } from 'ethers';
 import { Logger } from 'pino';
 
+import { SigningHyperlaneModuleClient } from '@hyperlane-xyz/cosmos-sdk';
 import {
   ChainName,
   ChainSubmissionStrategy,
+  MultiProtocolProvider,
   MultiProvider,
 } from '@hyperlane-xyz/sdk';
 import { ProtocolType, assert, rootLogger } from '@hyperlane-xyz/utils';
 
 import { ENV } from '../../../utils/env.js';
 
-import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js';
+import {
+  IMultiProtocolSigner,
+  SignerConfig,
+  TypedSigner,
+} from './BaseMultiProtocolSigner.js';
 import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js';
 
 export interface MultiProtocolSignerOptions {
@@ -24,13 +30,14 @@ export interface MultiProtocolSignerOptions {
  */
 export class MultiProtocolSignerManager {
   protected readonly signerStrategies: Map<ChainName, IMultiProtocolSigner>;
-  protected readonly signers: Map<ChainName, Signer>;
+  protected readonly signers: Map<ChainName, TypedSigner>;
   public readonly logger: Logger;
 
   constructor(
     protected readonly submissionStrategy: ChainSubmissionStrategy,
     protected readonly chains: ChainName[],
     protected readonly multiProvider: MultiProvider,
+    protected readonly multiProtocolProvider: MultiProtocolProvider,
     protected readonly options: MultiProtocolSignerOptions = {},
   ) {
     this.logger =
@@ -43,17 +50,19 @@ export class MultiProtocolSignerManager {
     this.initializeStrategies();
   }
 
+  protected get compatibleChains(): ChainName[] {
+    return this.chains.filter(
+      (chain) =>
+        this.multiProvider.getProtocol(chain) === ProtocolType.Ethereum ||
+        this.multiProvider.getProtocol(chain) === ProtocolType.CosmosNative,
+    );
+  }
+
   /**
    * @notice Sets up chain-specific signer strategies
    */
   protected initializeStrategies(): void {
-    for (const chain of this.chains) {
-      if (this.multiProvider.getProtocol(chain) !== ProtocolType.Ethereum) {
-        this.logger.debug(
-          `Skipping signer strategy initialization for non-EVM chain ${chain}`,
-        );
-        continue;
-      }
+    for (const chain of this.compatibleChains) {
       const strategy = MultiProtocolSignerFactory.getSignerStrategy(
         chain,
         this.submissionStrategy,
@@ -67,15 +76,9 @@ export class MultiProtocolSignerManager {
    * @dev Configures signers for EVM chains in MultiProvider
    */
   async getMultiProvider(): Promise<MultiProvider> {
-    for (const chain of this.chains) {
-      if (this.multiProvider.getProtocol(chain) !== ProtocolType.Ethereum) {
-        this.logger.debug(
-          `Skipping signer initialization for non-EVM chain ${chain}`,
-        );
-        continue;
-      }
+    for (const chain of this.compatibleChains) {
       const signer = await this.initSigner(chain);
-      this.multiProvider.setSigner(chain, signer);
+      this.multiProvider.setSigner(chain, signer as Signer);
     }
 
     return this.multiProvider;
@@ -84,82 +87,129 @@ export class MultiProtocolSignerManager {
   /**
    * @notice Creates signer for specific chain
    */
-  async initSigner(chain: ChainName): Promise<Signer> {
-    const { privateKey } = await this.resolveConfig(chain);
+  async initSigner(chain: ChainName): Promise<TypedSigner> {
+    const config = await this.resolveConfig(chain);
+    const signerStrategy = this.getSignerStrategyOrFail(chain);
+    const signer = await signerStrategy.getSigner(config);
 
-    const signerStrategy = this.signerStrategies.get(chain);
-    assert(signerStrategy, `No signer strategy found for chain ${chain}`);
-
-    return signerStrategy.getSigner({ privateKey });
+    this.signers.set(chain, signer);
+    return signer;
   }
 
   /**
    * @notice Creates signers for all chains
    */
   async initAllSigners(): Promise<typeof this.signers> {
-    const signerConfigs = await this.resolveAllConfigs();
-
-    for (const { chain, privateKey } of signerConfigs) {
+    for (const chain of this.compatibleChains) {
       const signerStrategy = this.signerStrategies.get(chain);
       if (signerStrategy) {
-        this.signers.set(chain, signerStrategy.getSigner({ privateKey }));
+        await this.initSigner(chain);
       }
     }
 
     return this.signers;
   }
 
-  /**
-   * @notice Resolves all chain configurations
-   */
-  private async resolveAllConfigs(): Promise<
-    Array<{ chain: ChainName; privateKey: string }>
-  > {
-    return Promise.all(this.chains.map((chain) => this.resolveConfig(chain)));
-  }
-
   /**
    * @notice Resolves single chain configuration
    */
   private async resolveConfig(
     chain: ChainName,
-  ): Promise<{ chain: ChainName; privateKey: string }> {
-    const signerStrategy = this.signerStrategies.get(chain);
-    assert(signerStrategy, `No signer strategy found for chain ${chain}`);
+  ): Promise<{ chain: ChainName } & SignerConfig> {
+    const { protocol } = this.multiProvider.getChainMetadata(chain);
 
-    let privateKey: string;
-
-    if (this.options.key) {
-      this.logger.debug(
-        `Using private key passed via CLI --key flag for chain ${chain}`,
-      );
-      privateKey = this.options.key;
-    } else if (ENV.HYP_KEY) {
-      this.logger.debug(`Using private key from .env for chain ${chain}`);
-      privateKey = ENV.HYP_KEY;
-    } else {
-      privateKey = await this.extractPrivateKey(chain, signerStrategy);
+    // For Cosmos, we must use strategy config
+    if (protocol === ProtocolType.CosmosNative) {
+      return this.resolveCosmosNativeConfig(chain, this.options.key);
     }
 
-    return { chain, privateKey };
+    // For other protocols, try CLI/ENV keys first, then fallback to strategy
+    const config = await this.extractPrivateKey(chain);
+    return { chain, ...config };
   }
 
   /**
    * @notice Gets private key from strategy
    */
-  private async extractPrivateKey(
-    chain: ChainName,
-    signerStrategy: IMultiProtocolSigner,
-  ): Promise<string> {
+  private async extractPrivateKey(chain: ChainName): Promise<SignerConfig> {
+    if (this.options.key) {
+      this.logger.info(
+        `Using private key passed via CLI --key flag for chain ${chain}`,
+      );
+      return { privateKey: this.options.key };
+    }
+
+    if (ENV.HYP_KEY) {
+      this.logger.info(`Using private key from .env for chain ${chain}`);
+      return { privateKey: ENV.HYP_KEY };
+    }
+
+    const signerStrategy = this.getSignerStrategyOrFail(chain);
     const strategyConfig = await signerStrategy.getSignerConfig(chain);
     assert(
       strategyConfig.privateKey,
       `No private key found for chain ${chain}`,
     );
-
-    this.logger.debug(
+    this.logger.info(
       `Extracting private key from strategy config/user prompt for chain ${chain}`,
     );
-    return strategyConfig.privateKey;
+
+    return { privateKey: strategyConfig.privateKey };
+  }
+
+  private async resolveCosmosNativeConfig(
+    chain: ChainName,
+    key?: string,
+  ): Promise<{ chain: ChainName } & SignerConfig> {
+    const signerStrategy = this.getSignerStrategyOrFail(chain);
+
+    if (!key) {
+      const strategyConfig = await signerStrategy.getSignerConfig(chain);
+      key = strategyConfig.privateKey;
+    }
+
+    const provider =
+      await this.multiProtocolProvider.getCosmJsNativeProvider(chain);
+    const { bech32Prefix, gasPrice } =
+      this.multiProvider.getChainMetadata(chain);
+
+    assert(key, `No private key found for chain ${chain}`);
+    assert(provider, 'No Cosmos Native Provider found');
+
+    this.logger.info(`Using strategy config for Cosmos Native chain ${chain}`);
+
+    return {
+      chain,
+      privateKey: key,
+      extraParams: { provider, prefix: bech32Prefix, gasPrice },
+    };
+  }
+
+  private getSignerStrategyOrFail(chain: ChainName): IMultiProtocolSigner {
+    const strategy = this.signerStrategies.get(chain);
+    assert(strategy, `No signer strategy found for chain ${chain}`);
+    return strategy;
+  }
+
+  protected getSpecificSigner<T>(chain: ChainName): T {
+    return this.signers.get(chain) as T;
+  }
+
+  getEVMSigner(chain: ChainName): Signer {
+    const protocolType = this.multiProvider.getChainMetadata(chain).protocol;
+    assert(
+      protocolType === ProtocolType.Ethereum,
+      `Chain ${chain} is not an Ethereum chain`,
+    );
+    return this.getSpecificSigner<Signer>(chain);
+  }
+
+  getCosmosNativeSigner(chain: ChainName): SigningHyperlaneModuleClient {
+    const protocolType = this.multiProvider.getProtocol(chain);
+    assert(
+      protocolType === ProtocolType.CosmosNative,
+      `Chain ${chain} is not a Cosmos Native chain`,
+    );
+    return this.getSpecificSigner<SigningHyperlaneModuleClient>(chain);
   }
 }
diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts
index 68858724957..14f792bf3f4 100644
--- a/typescript/cli/src/context/types.ts
+++ b/typescript/cli/src/context/types.ts
@@ -5,11 +5,14 @@ import type { IRegistry } from '@hyperlane-xyz/registry';
 import type {
   ChainMap,
   ChainMetadata,
+  MultiProtocolProvider,
   MultiProvider,
   WarpCoreConfig,
   WarpRouteDeployConfigMailboxRequired,
 } from '@hyperlane-xyz/sdk';
 
+import { MultiProtocolSignerManager } from './strategies/signer/MultiProtocolSignerManager.js';
+
 export interface ContextSettings {
   registryUris: string[];
   key?: string;
@@ -25,6 +28,7 @@ export interface CommandContext {
   registry: IRegistry;
   chainMetadata: ChainMap<ChainMetadata>;
   multiProvider: MultiProvider;
+  multiProtocolProvider: MultiProtocolProvider;
   skipConfirmation: boolean;
   key?: string;
   // just for evm chains backward compatibility
@@ -35,6 +39,7 @@ export interface CommandContext {
 export interface WriteCommandContext extends CommandContext {
   key: string;
   signer: ethers.Signer;
+  multiProtocolSigner?: MultiProtocolSignerManager;
   isDryRun?: boolean;
   dryRunChain?: string;
 }
diff --git a/yarn.lock b/yarn.lock
index 741f1df3003..02fabac6ffe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7641,9 +7641,11 @@ __metadata:
   dependencies:
     "@aws-sdk/client-kms": "npm:^3.577.0"
     "@aws-sdk/client-s3": "npm:^3.577.0"
+    "@cosmjs/stargate": "npm:^0.32.4"
     "@eslint/js": "npm:^9.15.0"
     "@ethersproject/abi": "npm:*"
     "@ethersproject/providers": "npm:*"
+    "@hyperlane-xyz/cosmos-sdk": "npm:12.5.0"
     "@hyperlane-xyz/registry": "npm:15.0.0"
     "@hyperlane-xyz/sdk": "npm:12.5.0"
     "@hyperlane-xyz/utils": "npm:12.5.0"


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T09:02:57Z
user: login: xeno097
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T15:47:39Z
user: login: troykessler
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T06:50:04Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T09:58:33Z
user: login: xeno097
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:39:25Z
user: login: xeno097
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:03:47Z
user: login: damiannolan
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-14T11:40:49Z
user: login: troykessler
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T13:01:03Z
user: login: damiannolan
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T13:45:04Z
user: login: damiannolan
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T13:46:11Z
user: login: damiannolan
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T13:47:49Z

Review comments:
diff_hunk: @@ -73,7 +78,48 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner {
     return { privateKey };
   }
 
-  getSigner(config: SignerConfig): Signer {
+  async getSigner(config: SignerConfig): Promise<Signer> {
     return new ZKSyncWallet(config.privateKey);
   }
 }
+
+class CosmosNativeSignerStrategy extends BaseMultiProtocolSigner {
+  async getSignerConfig(chain: ChainName): Promise<SignerConfig> {
+    const submitter = this.config[chain]?.submitter as {
+      privateKey?: string;
+    };
+
+    const privateKey =
+      submitter?.privateKey ??
+      (await password({
+        message: `Please enter the private key for chain ${chain}`,
+      }));
+
+    return {
+      privateKey,
+    };
+  }
+
+  async getSigner({
+    privateKey,
+    extraParams,
+  }: SignerConfig): Promise<SigningHyperlaneModuleClient> {
+    assert(
+      extraParams?.provider && extraParams?.prefix && extraParams?.gasPrice,
+      'Missing Cosmos Signer arguments',
+    );
+    const cometClient = extraParams?.provider.getCometClient();
+
+    // TODO: what if privateKey is mnemonic?
+    const wallet = await DirectSecp256k1Wallet.fromKey(
+      Buffer.from(privateKey, 'hex'),
+      extraParams.prefix,
+    );
+
+    return SigningHyperlaneModuleClient.createWithSigner(cometClient, wallet, {
+      gasPrice: GasPrice.fromString(
+        `${extraParams.gasPrice.amount}${extraParams.gasPrice.denom}`,
path: typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
user: login: graphite-app[bot]
body: The `getSigner` method in `CosmosNativeSignerStrategy` assumes `extraParams.gasPrice` has `amount` and `denom` properties, but this may not match the actual format of `gasPrice` in chain metadata. The code currently constructs a gas price string as `${extraParams.gasPrice.amount}${extraParams.gasPrice.denom}`, but `GasPrice.fromString()` typically expects a simple string like `"0.2uhyp"`.

Consider making this more robust by:
1. Ensuring chain metadata consistently provides `gasPrice` in the expected object format, or
2. Adding format detection to handle both string and object representations:

```typescript
// Handle both string and object formats
const gasPriceString = typeof extraParams.gasPrice === 'string' 
  ? extraParams.gasPrice
  : `${extraParams.gasPrice.amount}${extraParams.gasPrice.denom}`;

return SigningHyperlaneModuleClient.createWithSigner(cometClient, wallet, {
  gasPrice: GasPrice.fromString(gasPriceString),
});
```

This would prevent runtime errors when processing different gas price formats.
```suggestion
    assert(
      extraParams?.provider && extraParams?.prefix && extraParams?.gasPrice,
      'Missing Cosmos Signer arguments',
    );
    const cometClient = extraParams?.provider.getCometClient();

    // TODO: what if privateKey is mnemonic?
    const wallet = await DirectSecp256k1Wallet.fromKey(
      Buffer.from(privateKey, 'hex'),
      extraParams.prefix,
    );

    // Handle both string and object formats
    const gasPriceString =
      typeof extraParams.gasPrice === 'string'
        ? extraParams.gasPrice
        : `${extraParams.gasPrice.amount}${extraParams.gasPrice.denom}`;

    return SigningHyperlaneModuleClient.createWithSigner(cometClient, wallet, {
      gasPrice: GasPrice.fromString(gasPriceString),
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=hyperlane-xyz&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-05T09:02:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,6 +31,8 @@ export interface CommandContext {
   // just for evm chains backward compatibility
   signerAddress?: string;
   strategyPath?: string;
+  multiProtocolProvider?: MultiProtocolProvider;
+  multiProtocolSigner?: MultiProtocolSignerManager;
path: typescript/cli/src/context/types.ts
user: login: xeno097
body: Do we need these in readonly commands (warp read etc)? if not can we add these fields as required but in the `WriteCommandContext` type below?
created_at: 2025-05-06T15:39:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,6 +31,8 @@ export interface CommandContext {
   // just for evm chains backward compatibility
   signerAddress?: string;
   strategyPath?: string;
+  multiProtocolProvider?: MultiProtocolProvider;
+  multiProtocolSigner?: MultiProtocolSignerManager;
path: typescript/cli/src/context/types.ts
user: login: troykessler
body: made `MultiProtocolProvider ` required in `CommandContext`, but `MultiProtocolSignerManager ` is still optional in `WriteCommandContext` because of the dry run
created_at: 2025-05-07T06:50:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,8 +1,11 @@
 import { Signer } from 'ethers';
 
+import { SigningHyperlaneModuleClient } from '@hyperlane-xyz/cosmos-sdk';
 import { ChainName, ChainSubmissionStrategy } from '@hyperlane-xyz/sdk';
 import { Address } from '@hyperlane-xyz/utils';
 
+export type TypedSigner = Signer | SigningHyperlaneModuleClient;
+
 export interface SignerConfig {
   privateKey: string;
   address?: Address; // For chains like StarkNet that require address
path: typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts
user: login: graphite-app[bot]
body: The `SignerConfig` interface is missing the `extraParams` property that's used in the `CosmosNativeSignerStrategy` implementation. Consider adding `extraParams?: Record<string, any>;` to the interface to properly type the additional parameters needed for Cosmos signers, which would improve type safety and make the interface contract more explicit.
```suggestion

export interface SignerConfig {
  privateKey: string;
  address?: Address; // For chains like StarkNet that require address
  extraParams?: Record<string, any>;
}

```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=hyperlane-xyz&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-12T09:58:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,12 +55,18 @@ export class MultiProtocolSignerManager {
    */
   protected initializeStrategies(): void {
     for (const chain of this.chains) {
-      if (this.multiProvider.getProtocol(chain) !== ProtocolType.Ethereum) {
+      const protocolType = this.multiProvider.getProtocol(chain);
+
+      if (
+        protocolType !== ProtocolType.Ethereum &&
+        protocolType !== ProtocolType.CosmosNative
path: typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts
user: login: xeno097
body: Considering that this check is similar to the one below, maybe it would be better to a single function that checks if the current protocol type is supported for initializing a strategy/signer so that we don't have to update this in multiple places, wdyt?
created_at: 2025-05-12T16:39:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,7 +81,59 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner {
     return { privateKey };
   }
 
-  getSigner(config: SignerConfig): Signer {
+  async getSigner(config: SignerConfig): Promise<Signer> {
     return new ZKSyncWallet(config.privateKey);
   }
 }
+
+class CosmosNativeSignerStrategy extends BaseMultiProtocolSigner {
+  async getSignerConfig(chain: ChainName): Promise<SignerConfig> {
+    const submitter = this.config[chain]?.submitter as {
+      privateKey?: string;
+    };
+
+    const privateKey =
+      submitter?.privateKey ??
+      (await password({
+        message: `Please enter the private key for chain ${chain}`,
+      }));
+
+    return {
+      privateKey,
+    };
+  }
+
+  async getSigner({
+    privateKey,
+    extraParams,
+  }: SignerConfig): Promise<SigningHyperlaneModuleClient> {
+    assert(
+      extraParams?.provider && extraParams?.prefix && extraParams?.gasPrice,
+      'Missing Cosmos Signer arguments',
+    );
+
+    let wallet;
+
+    if (ethers.utils.isHexString(ensure0x(privateKey))) {
+      wallet = await DirectSecp256k1Wallet.fromKey(
+        Buffer.from(privateKey, 'hex'),
path: typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
user: login: damiannolan
body: Hi! I was running some of this code locally and ran into an issue: `Error: input data is not a valid secp256k1 private key`

I fixed it by removing the 0x prefix. Afaik `Buffer.from()` requires non 0x prefixed hex strings.

https://nodejs.org/api/buffer.html#static-method-bufferfromstring-encoding
created_at: 2025-05-14T11:40:49Z
author_association: NONE
diff_hunk: @@ -73,7 +81,59 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner {
     return { privateKey };
   }
 
-  getSigner(config: SignerConfig): Signer {
+  async getSigner(config: SignerConfig): Promise<Signer> {
     return new ZKSyncWallet(config.privateKey);
   }
 }
+
+class CosmosNativeSignerStrategy extends BaseMultiProtocolSigner {
+  async getSignerConfig(chain: ChainName): Promise<SignerConfig> {
+    const submitter = this.config[chain]?.submitter as {
+      privateKey?: string;
+    };
+
+    const privateKey =
+      submitter?.privateKey ??
+      (await password({
+        message: `Please enter the private key for chain ${chain}`,
+      }));
+
+    return {
+      privateKey,
+    };
+  }
+
+  async getSigner({
+    privateKey,
+    extraParams,
+  }: SignerConfig): Promise<SigningHyperlaneModuleClient> {
+    assert(
+      extraParams?.provider && extraParams?.prefix && extraParams?.gasPrice,
+      'Missing Cosmos Signer arguments',
+    );
+
+    let wallet;
+
+    if (ethers.utils.isHexString(ensure0x(privateKey))) {
+      wallet = await DirectSecp256k1Wallet.fromKey(
+        Buffer.from(privateKey, 'hex'),
path: typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
user: login: troykessler
body: Hey, can you send me the command you used which gave you the error? And if you used the `-k` flag for the private key, did it had a `0x` prefix?
created_at: 2025-05-15T13:01:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,7 +81,59 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner {
     return { privateKey };
   }
 
-  getSigner(config: SignerConfig): Signer {
+  async getSigner(config: SignerConfig): Promise<Signer> {
     return new ZKSyncWallet(config.privateKey);
   }
 }
+
+class CosmosNativeSignerStrategy extends BaseMultiProtocolSigner {
+  async getSignerConfig(chain: ChainName): Promise<SignerConfig> {
+    const submitter = this.config[chain]?.submitter as {
+      privateKey?: string;
+    };
+
+    const privateKey =
+      submitter?.privateKey ??
+      (await password({
+        message: `Please enter the private key for chain ${chain}`,
+      }));
+
+    return {
+      privateKey,
+    };
+  }
+
+  async getSigner({
+    privateKey,
+    extraParams,
+  }: SignerConfig): Promise<SigningHyperlaneModuleClient> {
+    assert(
+      extraParams?.provider && extraParams?.prefix && extraParams?.gasPrice,
+      'Missing Cosmos Signer arguments',
+    );
+
+    let wallet;
+
+    if (ethers.utils.isHexString(ensure0x(privateKey))) {
+      wallet = await DirectSecp256k1Wallet.fromKey(
+        Buffer.from(privateKey, 'hex'),
path: typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
user: login: damiannolan
body: Sure! IIRC I checked out https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/6050 and tried using `hyperlane core deploy` with a local strategy file. Checking my history... 
created_at: 2025-05-15T13:45:04Z
author_association: NONE
diff_hunk: @@ -73,7 +81,59 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner {
     return { privateKey };
   }
 
-  getSigner(config: SignerConfig): Signer {
+  async getSigner(config: SignerConfig): Promise<Signer> {
     return new ZKSyncWallet(config.privateKey);
   }
 }
+
+class CosmosNativeSignerStrategy extends BaseMultiProtocolSigner {
+  async getSignerConfig(chain: ChainName): Promise<SignerConfig> {
+    const submitter = this.config[chain]?.submitter as {
+      privateKey?: string;
+    };
+
+    const privateKey =
+      submitter?.privateKey ??
+      (await password({
+        message: `Please enter the private key for chain ${chain}`,
+      }));
+
+    return {
+      privateKey,
+    };
+  }
+
+  async getSigner({
+    privateKey,
+    extraParams,
+  }: SignerConfig): Promise<SigningHyperlaneModuleClient> {
+    assert(
+      extraParams?.provider && extraParams?.prefix && extraParams?.gasPrice,
+      'Missing Cosmos Signer arguments',
+    );
+
+    let wallet;
+
+    if (ethers.utils.isHexString(ensure0x(privateKey))) {
+      wallet = await DirectSecp256k1Wallet.fromKey(
+        Buffer.from(privateKey, 'hex'),
path: typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
user: login: damiannolan
body: Before the code change:

```
yarn hyperlane core deploy --chain celestia --registry $HYP_REG --verbosity trace
Hyperlane CLI
Reading submission strategy in /Users/damiannolan/.hyperlane/strategies/default-strategy.yaml
Using strategy config for Cosmos Native chain celestia
privKey: 0x82bfcfadbf1712f6550d8d2c00a39f05b33ec78939d0167be2a737d691f33a6a
Error: input data is not a valid secp256k1 private key
    at Secp256k1.makeKeypair (/Users/damiannolan/development/hyperlane-monorepo/node_modules/@cosmjs/crypto/build/secp256k1.js:24:19)
    at DirectSecp256k1Wallet.fromKey (/Users/damiannolan/development/hyperlane-monorepo/node_modules/@cosmjs/proto-signing/build/directsecp256k1wallet.js:21:56)
    at CosmosNativeSignerStrategy.getSigner (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/cli/dist/src/context/strategies/signer/MultiProtocolSignerFactory.js:68:50)
    at MultiProtocolSignerManager.initSigner (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/cli/dist/src/context/strategies/signer/MultiProtocolSignerManager.js:61:45)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async MultiProtocolSignerManager.initAllSigners (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/cli/dist/src/context/strategies/signer/MultiProtocolSignerManager.js:72:17)
    at async signerMiddleware (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/cli/dist/src/context/context.js:56:5)
    at async Promise.all (index 1)
```

After the code change:

```
yarn hyperlane core deploy --chain celestia --registry $HYP_REG --verbosity trace
Hyperlane CLI
Reading submission strategy in /Users/damiannolan/.hyperlane/strategies/default-strategy.yaml
Using strategy config for Cosmos Native chain celestia
privKey: 0x82bfcfadbf1712f6550d8d2c00a39f05b33ec78939d0167be2a737d691f33a6a
Using strategy config for Cosmos Native chain celestia
privKey: 0x82bfcfadbf1712f6550d8d2c00a39f05b33ec78939d0167be2a737d691f33a6a
Hyperlane Core deployment
_________________________
? Do you want to use an API key to verify on this (celestia) chain's block explorer no
🚀 All systems ready, captain! Beginning deployment...
Deploying trustedRelayerIsm to celestia
Error: ISM type trustedRelayerIsm is not supported on Cosmos Native
    at CosmosNativeIsmModule.deploy (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/sdk/dist/ism/CosmosNativeIsmModule.js:105:23)
    at CosmosNativeIsmModule.create (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/sdk/dist/ism/CosmosNativeIsmModule.js:82:58)
    at CosmosNativeCoreModule.deploy (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/sdk/dist/core/CosmosNativeCoreModule.js:63:55)
    at CosmosNativeCoreModule.create (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/sdk/dist/core/CosmosNativeCoreModule.js:40:56)
    at runCoreDeploy (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/cli/dist/src/deploy/core.js:69:77)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async Object.handler (file:///Users/damiannolan/development/hyperlane-monorepo/typescript/cli/dist/src/commands/core.js:70:13)
```
created_at: 2025-05-15T13:46:11Z
author_association: NONE
diff_hunk: @@ -73,7 +81,59 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner {
     return { privateKey };
   }
 
-  getSigner(config: SignerConfig): Signer {
+  async getSigner(config: SignerConfig): Promise<Signer> {
     return new ZKSyncWallet(config.privateKey);
   }
 }
+
+class CosmosNativeSignerStrategy extends BaseMultiProtocolSigner {
+  async getSignerConfig(chain: ChainName): Promise<SignerConfig> {
+    const submitter = this.config[chain]?.submitter as {
+      privateKey?: string;
+    };
+
+    const privateKey =
+      submitter?.privateKey ??
+      (await password({
+        message: `Please enter the private key for chain ${chain}`,
+      }));
+
+    return {
+      privateKey,
+    };
+  }
+
+  async getSigner({
+    privateKey,
+    extraParams,
+  }: SignerConfig): Promise<SigningHyperlaneModuleClient> {
+    assert(
+      extraParams?.provider && extraParams?.prefix && extraParams?.gasPrice,
+      'Missing Cosmos Signer arguments',
+    );
+
+    let wallet;
+
+    if (ethers.utils.isHexString(ensure0x(privateKey))) {
+      wallet = await DirectSecp256k1Wallet.fromKey(
+        Buffer.from(privateKey, 'hex'),
path: typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
user: login: damiannolan
body: The strategy file I had was this I believe:

```
celestia:
  submitter:
    chain: celestia
    privateKey: 00ae97487709f19987c2d62caab232d2bb5e489fc319480970782096bf1eda6a
    type: jsonRpc
    userAddress: celestia13vedhr3w833pdwlr9ctnqjc5ezne8sfz7n63vj
```

The logs above are a bit misleading as I also have this for the evm env
```
echo $HYP_KEY
0x82bfcfadbf1712f6550d8d2c00a39f05b33ec78939d0167be2a737d691f33a6a
```

I don't think I was providing the `--key` flag, as it was picked up by the file
created_at: 2025-05-15T13:47:49Z
author_association: NONE

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-05T08:57:09Z
body: ###  ⚠️  No Changeset found

Latest commit: a98e12e6c11713ad81eea2cec07c65bf10bd1db3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/hyperlane-xyz/hyperlane-monorepo/new/troy/cosmos-signer-strategy?filename=.changeset/silly-zebras-destroy.md&value=---%0A%22%40hyperlane-xyz%2Fcli%22%3A%20patch%0A---%0A%0Achore%3A%20add%20cosmos%20native%20signer%20strategy%20to%20cli%0A)


author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-05T09:09:28Z
body: ## [Codecov](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6124?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 76.63%. Comparing base [(`e35ad1a`)](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/commit/e35ad1af589d6af70d36087e129e78add0002dfb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) to head [(`a98e12e`)](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/commit/a98e12e6c11713ad81eea2cec07c65bf10bd1db3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz).
> Report is 10 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #6124   +/-   ##
=======================================
  Coverage   76.63%   76.63%           
=======================================
  Files         110      110           
  Lines        2166     2166           
  Branches      196      196           
=======================================
  Hits         1660     1660           
  Misses        485      485           
  Partials       21       21           
```

| [Components](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6124/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | Coverage Δ | |
|---|---|---|
| [core](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6124/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `87.80% <ø> (ø)` | |
| [hooks](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6124/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `78.14% <ø> (ø)` | |
| [isms](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6124/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `79.94% <ø> (ø)` | |
| [token](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6124/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `90.54% <ø> (ø)` | |
| [middlewares](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6124/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `79.80% <ø> (ø)` | |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE