Extension Wallet
Integration
COSMOS Chains
Wallet

Wallet

The interface of each Cosmos wallet is different, and in order to solve the problem of having to manually attach each wallet to the Dapp, it was developed to automatically add the same interface and wallet.

This section describes @cosmostation/wallets, a small utility layer that lets you register external Cosmos wallets (e.g. Keplr, Leap) into the same normalized interface used by @cosmostation/use-wallets.

In other words:

  • different wallets expose different APIs
  • this package wraps them into a common CosmosRegisterWallet shape
  • your dApp can then interact with them through a unified interface

This is especially useful when you want a single React hook / UI flow to support multiple wallet extensions without custom code per wallet.


Installation

npm install @cosmostation/wallets

Or if you're using yarn:

yarn add @cosmostation/wallets

Add Wallet

To detect a wallet, it is ideal to inject it directly from the extension, but it is also possible to manually inject a wallet that has not been injected into a dapp.

Most wallet extensions inject themselves automatically (e.g. window.keplr, window.leap). However, even if the wallet is present, your dApp still needs a way to:

  • standardize events (account changed, keystore change)
  • standardize methods (connect, getAccount, sign, sendTx, addChain, etc.)

registerCosmosWallet() lets you manually register a wallet adapter so it becomes discoverable by the rest of the Cosmostation wallet tooling.

Example

import { registerCosmosWallet, CosmosRegisterWallet } from '@cosmostation/wallets';
 
<button
  onClick={() => {
    const wallet: CosmosRegisterWallet = {
      // ...
    };
 
    registerCosmosWallet(wallet);
  }}
>
  Register Wallet
</button>;
Registered Wallets

go to test

After registration, you can validate that the wallet appears in the wallet list exposed by useCosmosWallets() (React Hook section).


Example

This is an example of adding keplr and leap wallet.

The examples below demonstrate a common adapter pattern:

  1. Check whether the extension is installed (e.g. window.keplr)

  2. Provide events.on/off wiring for account changes

  3. Implement standardized methods:

    • getSupportedChainIds (feature gating)
    • connect (enable + permissions)
    • getAccount (address / pubkey normalization)
    • signAmino / signDirect (signing abstractions)
    • sendTransaction (broadcast mode mapping + txhash normalization)
    • addChain (suggest chain / experimentalSuggestChain wrapper)

Once this adapter is registered, your dApp can treat Keplr/Leap like “just another wallet” through a unified Cosmostation-compatible interface.

Keplr

import { registerCosmosWallet, CosmosRegisterWallet } from '@cosmostation/wallets';
 
<button
  onClick={() => {
    if (!window.keplr) {
      alert('Keplr extension is not installed');
      return;
    }
 
    const wallet: CosmosRegisterWallet = {
      name: 'Keplr',
      logo: 'https://wallet.keplr.app/keplr-brand-assets/keplr-logo-v2.svg',
      events: {
        on(type, listener) {
          if (type === 'AccountChanged') {
            window.addEventListener('keplr_keystorechange', listener);
          }
        },
        off(type, listener) {
          if (type === 'AccountChanged') {
            window.removeEventListener('keplr_keystorechange', listener);
          }
        },
      },
      methods: {
        getSupportedChainIds: async () => {
          return ['cosmoshub-4'];
        },
        connect: async (chainIds) => {
          const cIds = typeof chainIds === 'string' ? [chainIds] : chainIds;
          const supportedChainIds = await wallet.methods.getSupportedChainIds();
 
          if (!cIds.every((cId) => supportedChainIds.includes(cId))) {
            throw new Error('Unsupported chainId is exist');
          }
 
          await window.keplr.enable(chainIds);
        },
        getAccount: async (chainId) => {
          const response = await window.keplr.getKey(chainId);
 
          return {
            address: response.bech32Address,
            name: response.name,
            public_key: {
              type: response.algo,
              value: Buffer.from(response.pubKey).toString('base64'),
            },
            is_ledger: response.isNanoLedger,
          };
        },
        signAmino: async (chainId, document, options) => {
          if (typeof options?.edit_mode?.fee === 'boolean') {
            window.keplr.defaultOptions.sign.preferNoSetFee = options.edit_mode.fee;
          }
 
          if (typeof options?.edit_mode?.memo === 'boolean') {
            window.keplr.defaultOptions.sign.preferNoSetMemo = options.edit_mode.memo;
          }
 
          if (typeof options?.is_check_balance === 'boolean') {
            window.keplr.defaultOptions.sign.disableBalanceCheck = options.is_check_balance;
          }
 
          const signer = options?.signer || (await wallet.methods.getAccount(chainId)).address;
 
          const response = await window.keplr.signAmino(chainId, signer, document);
 
          return {
            signature: response.signature.signature,
            signed_doc: response.signed,
          };
        },
        signDirect: async (chainId, document, options) => {
          if (typeof options?.edit_mode?.fee === 'boolean') {
            window.keplr.defaultOptions.sign.preferNoSetFee = options.edit_mode.fee;
          }
 
          if (typeof options?.edit_mode?.memo === 'boolean') {
            window.keplr.defaultOptions.sign.preferNoSetMemo = options.edit_mode.memo;
          }
 
          if (typeof options?.is_check_balance === 'boolean') {
            window.keplr.defaultOptions.sign.disableBalanceCheck = !options.is_check_balance;
          }
 
          const account = await wallet.methods.getAccount(chainId);
 
          if (account.is_ledger) {
            throw new Error('Ledger is not supported');
          }
 
          const signer = options?.signer || account.address;
 
          const signingDoc = {
            accountNumber: document.account_number,
            authInfoBytes: document.auth_info_bytes,
            chainId: document.chain_id,
            bodyBytes: document.body_bytes,
          };
 
          const response = await window.keplr.signDirect(chainId, signer, signingDoc);
 
          return {
            signature: response.signature.signature,
            signed_doc: {
              auth_info_bytes: response.signed.authInfoBytes,
              body_bytes: response.signed.bodyBytes,
            },
          };
        },
        sendTransaction: async (chainId, tx_bytes, mode) => {
          const broadcastMode =
            mode === 1 ? 'block' : mode === 2 ? 'sync' : mode === 3 ? 'async' : 'sync';
 
          const txBytes =
            typeof tx_bytes === 'string'
              ? new Uint8Array(Buffer.from(tx_bytes, 'base64'))
              : tx_bytes;
 
          const response = await window.keplr.sendTx(chainId, txBytes, broadcastMode);
 
          const txHash = Buffer.from(response).toString('hex').toUpperCase();
 
          return txHash;
        },
        addChain: async (chain) => {
          const coinType = chain.coin_type ? Number(chain.coin_type.replaceAll("'", '')) : 118;
 
          await window.keplr.experimentalSuggestChain({
            chainId: chain.chain_id,
            chainName: chain.chain_name,
            rpc: chain.lcd_url,
            rest: chain.lcd_url,
            bip44: {
              coinType,
            },
            bech32Config: {
              bech32PrefixAccAddr: chain.address_prefix,
              bech32PrefixAccPub: chain.address_prefix + 'pub',
              bech32PrefixValAddr: chain.address_prefix + 'valoper',
              bech32PrefixValPub: chain.address_prefix + 'valoperpub',
              bech32PrefixConsAddr: chain.address_prefix + 'valcons',
              bech32PrefixConsPub: chain.address_prefix + 'valconspub',
            },
            currencies: [
              {
                coinDenom: chain.display_denom,
                coinMinimalDenom: chain.base_denom,
                coinDecimals: chain.decimals || 6,
                coinGeckoId: chain.coingecko_id || 'unknown',
              },
            ],
            feeCurrencies: [
              {
                coinDenom: chain.display_denom,
                coinMinimalDenom: chain.base_denom,
                coinDecimals: chain.decimals || 6,
                coinGeckoId: chain.coingecko_id || 'unknown',
                gasPriceStep: {
                  low: chain?.gas_rate?.tiny ? Number(chain?.gas_rate?.tiny) : 0.01,
                  average: chain?.gas_rate?.low ? Number(chain?.gas_rate?.low) : 0.025,
                  high: chain?.gas_rate?.average ? Number(chain?.gas_rate?.average) : 0.04,
                },
              },
            ],
            stakeCurrency: {
              coinDenom: chain.display_denom,
              coinMinimalDenom: chain.base_denom,
              coinDecimals: chain.decimals || 6,
              coinGeckoId: chain.coingecko_id || 'unknown',
            },
          });
        },
      },
    };
 
    registerCosmosWallet(wallet);
  }}
>
  Register Keplr Wallet
</button>;

Notes:

  • events.on/off maps Keplr’s keplr_keystorechange into the unified AccountChanged event.
  • sendTransaction normalizes tx bytes and returns an uppercase hex tx hash.
  • addChain uses experimentalSuggestChain, which may vary by wallet version.

Leap

import { registerCosmosWallet, CosmosRegisterWallet } from '@cosmostation/wallets';
 
<button
  onClick={() => {
    if (!window.leap) {
      alert('Leap extension is not installed');
      return;
    }
 
    const wallet: CosmosRegisterWallet = {
      name: 'Leap',
      logo: 'https://miro.medium.com/v2/resize:fill:176:176/1*2jNLyjIPuU8HBbayPapwcQ.png',
      events: {
        on(type, listener) {
          if (type === 'AccountChanged') {
            window.addEventListener('leap_keystorechange', listener);
          }
        },
        off(type, listener) {
          if (type === 'AccountChanged') {
            window.removeEventListener('leap_keystorechange', listener);
          }
        },
      },
      methods: {
        getSupportedChainIds: async () => {
          return ['cosmoshub-4'];
        },
        connect: async (chainIds) => {
          const cIds = typeof chainIds === 'string' ? [chainIds] : chainIds;
          const supportedChainIds = await wallet.methods.getSupportedChainIds();
 
          if (!cIds.every((cId) => supportedChainIds.includes(cId))) {
            throw new Error('Unsupported chainId is exist');
          }
 
          await window.leap.enable(chainIds);
        },
        getAccount: async (chainId) => {
          const response = await window.leap.getKey(chainId);
 
          return {
            address: response.bech32Address,
            name: response.name,
            public_key: {
              type: response.algo,
              value: Buffer.from(response.pubKey).toString('base64'),
            },
            is_ledger: response.isNanoLedger,
          };
        },
        signAmino: async (chainId, document, options) => {
          if (typeof options?.edit_mode?.fee === 'boolean') {
            window.leap.defaultOptions.sign.preferNoSetFee = options.edit_mode.fee;
          }
 
          if (typeof options?.edit_mode?.memo === 'boolean') {
            window.leap.defaultOptions.sign.preferNoSetMemo = options.edit_mode.memo;
          }
 
          if (typeof options?.is_check_balance === 'boolean') {
            window.leap.defaultOptions.sign.disableBalanceCheck = options.is_check_balance;
          }
 
          const signer = options?.signer || (await wallet.methods.getAccount(chainId)).address;
 
          const response = await window.leap.signAmino(chainId, signer, document);
 
          return {
            signature: response.signature.signature,
            signed_doc: response.signed,
          };
        },
        signDirect: async (chainId, document, options) => {
          if (typeof options?.edit_mode?.fee === 'boolean') {
            window.leap.defaultOptions.sign.preferNoSetFee = options.edit_mode.fee;
          }
 
          if (typeof options?.edit_mode?.memo === 'boolean') {
            window.leap.defaultOptions.sign.preferNoSetMemo = options.edit_mode.memo;
          }
 
          if (typeof options?.is_check_balance === 'boolean') {
            window.leap.defaultOptions.sign.disableBalanceCheck = !options.is_check_balance;
          }
 
          const account = await wallet.methods.getAccount(chainId);
 
          if (account.is_ledger) {
            throw new Error('Ledger is not supported');
          }
 
          const signer = options?.signer || account.address;
 
          const signingDoc = {
            accountNumber: document.account_number,
            authInfoBytes: document.auth_info_bytes,
            chainId: document.chain_id,
            bodyBytes: document.body_bytes,
          };
 
          const response = await window.leap.signDirect(chainId, signer, signingDoc);
 
          return {
            signature: response.signature.signature,
            signed_doc: {
              auth_info_bytes: response.signed.authInfoBytes,
              body_bytes: response.signed.bodyBytes,
            },
          };
        },
        sendTransaction: async (chainId, tx_bytes, mode) => {
          const broadcastMode =
            mode === 1 ? 'block' : mode === 2 ? 'sync' : mode === 3 ? 'async' : 'sync';
 
          const txBytes =
            typeof tx_bytes === 'string'
              ? new Uint8Array(Buffer.from(tx_bytes, 'base64'))
              : tx_bytes;
 
          const response = await window.leap.sendTx(chainId, txBytes, broadcastMode);
 
          const txHash = Buffer.from(response).toString('hex').toUpperCase();
 
          return txHash;
        },
        addChain: async (chain) => {
          const coinType = chain.coin_type ? Number(chain.coin_type.replaceAll("'", '')) : 118;
 
          await window.leap.experimentalSuggestChain({
            chainId: chain.chain_id,
            chainName: chain.chain_name,
            rpc: chain.lcd_url,
            rest: chain.lcd_url,
            bip44: {
              coinType,
            },
            bech32Config: {
              bech32PrefixAccAddr: chain.address_prefix,
              bech32PrefixAccPub: chain.address_prefix + 'pub',
              bech32PrefixValAddr: chain.address_prefix + 'valoper',
              bech32PrefixValPub: chain.address_prefix + 'valoperpub',
              bech32PrefixConsAddr: chain.address_prefix + 'valcons',
              bech32PrefixConsPub: chain.address_prefix + 'valconspub',
            },
            currencies: [
              {
                coinDenom: chain.display_denom,
                coinMinimalDenom: chain.base_denom,
                coinDecimals: chain.decimals || 6,
                coinGeckoId: chain.coingecko_id || 'unknown',
              },
            ],
            feeCurrencies: [
              {
                coinDenom: chain.display_denom,
                coinMinimalDenom: chain.base_denom,
                coinDecimals: chain.decimals || 6,
                coinGeckoId: chain.coingecko_id || 'unknown',
                gasPriceStep: {
                  low: chain?.gas_rate?.tiny ? Number(chain?.gas_rate?.tiny) : 0.01,
                  average: chain?.gas_rate?.low ? Number(chain?.gas_rate?.low) : 0.025,
                  high: chain?.gas_rate?.average ? Number(chain?.gas_rate?.average) : 0.04,
                },
              },
            ],
            stakeCurrency: {
              coinDenom: chain.display_denom,
              coinMinimalDenom: chain.base_denom,
              coinDecimals: chain.decimals || 6,
              coinGeckoId: chain.coingecko_id || 'unknown',
            },
          });
        },
      },
    };
 
    registerCosmosWallet(wallet);
  }}
>
  Register Leap Wallet
</button>;

Notes:

  • Leap also emits a keystore change event (leap_keystorechange) which is mapped to AccountChanged.
  • Method signatures are intentionally aligned with the unified interface so your app logic stays the same across wallets.