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
CosmosRegisterWalletshape- 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/walletsOr if you're using yarn:
yarn add @cosmostation/walletsAdd 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.
import { registerCosmosWallet, CosmosRegisterWallet } from '@cosmostation/wallets';
<button
onClick={() => {
const wallet: CosmosRegisterWallet = {
// ...
};
registerCosmosWallet(wallet);
}}
>
Register Wallet
</button>;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:
Check whether the extension is installed (e.g.
window.keplr)Provide
events.on/offwiring for account changesImplement 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/offmaps Keplr’skeplr_keystorechangeinto the unifiedAccountChangedevent.sendTransactionnormalizes tx bytes and returns an uppercase hex tx hash.addChainusesexperimentalSuggestChain, 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 toAccountChanged.- Method signatures are intentionally aligned with the unified interface so your app logic stays the same across wallets.