Wallet Integration
Note: want to learn more about wallet integration beyond what's covered here? Check out both the MetaMask Wallet Documentation and Keplr Wallet Documentation.
Installation
Follow the build and installation instructions specified in EvmosJS to get setup.
Packages
Visit our documentation on client integrations to learn more about EvmosJS and the libraries we recommend in integrations.
Overview
The integration checklist for dApp developers consists of three categories:
Frontend
Make sure to create a wallet-connection button for Metamask and/or Keplr on the frontend of the application. For instance, consider the "Connect to a wallet" button on the interface of EvmoSwap or the analogous button on the interface of Evmos Dashboard Assets.
Identify the user's wallet
Developers should determine whether users are using Keplr or MetaMask.
This can be determined by checking the corresponding window.ethereum
or window.keplr
value.
If either window.ethereum
or window.keplr
returns undefined
after document.load
,
then MetaMask (or Keplr) is not installed.
There are several ways to wait for the load event to check the status:
developers can register functions to window.onload
,
or they can track the document's ready state through the document event listener.
If developer is checking for the document state's readiness,
then document.readyState === 'complete'
is a suitable check.
const userHasMetaMask = window.ethereum;
const userHasKeplr = window.keplr;
if (!userHasMetaMask && !userHasKeplr) {
// Handle error accordingly.
// ...
}
After determining the wallet type, developers can list a user's connected accounts.
Get connected accounts
Note: The example below uses the Evmos Mainnet chainID
.
For more info, check the Evmos Chain IDs reference document
here.
Developers can fetch a user's address in order to query account information and sign transactions.
Keplr
const chainId = 'evmos_9001-2'
await window.keplr.enable(chainId)
const account = await window.keplr.getKey(chainId)
// See the account schema here:
// https://github.com/chainapsis/keplr-wallet/blob/77b91ddbb031fb1c539f22b0f9b0988919322ecc/packages/types/src/wallet/keplr.ts#L15
MetaMask
import { ethToEvmos } from '@evmos/address-converter'
// Note that MetaMask uses numeric ChainIDs.
const chainId = 9001
await window.ethereum.enable(chainId)
// Accounts is a list of Ethereum hex addresses.
const accounts = await window?.ethereum?.request({
method: 'eth_requestAccounts',
})
// Address is a Bech32-encoded Evmos address.
const address = ethToEvmos(accounts[0])
Queries
After fetching a user's address, developers can query the node for a user's account fields, in order to generate Cosmos Transaction payloads.
Query Account Information
import { generateEndpointAccount } from '@evmos/provider'
const address = 'evmos1...'
// Find node urls for either mainnet or testnet here:
// https://docs.evmos.org/develop/api/networks.
const nodeUrl = '...'
const queryEndpoint = `${nodeUrl}${generateEndpointAccount(address)}`
const restOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
// Note that the node will return a 400 status code if the account does not exist.
const rawResult = await fetch(
queryEndpoint,
restOptions,
)
const result = await rawResult.json()
// The response format is available at @evmos/provider/rest/account/AccountResponse.
// Note that the `pub_key` will be `null` if the address has not sent any transactions.
/*
account: {
'@type': string
base_account: {
address: string
pub_key?: {
'@type': string
key: string
}
account_number: string
sequence: string
}
code_hash: string
}
*/
Retrieve a user's public key
If the public key is not returned with the query above, developers must retrieve the public key using the wallet. The public key is needed to sign Cosmos transactions.
Keplr
const chainID = 'evmos_9001-2'
const account = await window?.keplr?.getKey(chainID)
const pk = Buffer.from(account.pubKey).toString('base64')
MetaMask
import { hashMessage } from '@ethersproject/hash'
import {
computePublicKey,
recoverPublicKey,
} from '@ethersproject/signing-key'
// ...
const accounts = await window?.ethereum?.request({
method: 'eth_requestAccounts',
})
// Handle errors if MetaMask fails to return any accounts.
// Since MetaMask does not provide an interface to retrieve a user's
// public key, we must sign a message and ecrecover the key.
const message = 'Verify Public Key'
const signature = await window?.ethereum?.request({
method: 'personal_sign',
params: [message, accounts[0], ''],
})
const uncompressedPk = recoverPublicKey(
hashMessage(message),
signature,
)
const hexPk = computePublicKey(uncompressedPk, true)
const pk = Buffer.from(
hexPk.replace('0x', ''), 'hex',
).toString('base64')
Send Cosmos Transactions
After obtaining a user's account information, developers can begin creating, signing, and broadcasting transactions.
Create the Transaction
Developers can create transaction payloads using the official EvmosJS library. Here is an example, using the Cosmos MsgSend payload.
import {
Chain,
Sender,
Fee,
TxContext,
MsgSendParams,
createTxMsgSend,
TxPayload,
} from '@evmos/transactions'
const chain: Chain = {
chainId: 9001,
cosmosChainId: 'evmos_9001-2',
}
// Populate the transaction sender parameters using the
// query API.
const sender: Sender = {
accountAddress: <sender_account_address>,
sequence: <sender_sequence>,
accountNumber: <sender_account_number>,
// Use an empty string if the pubkey is unknown.
pubkey: <sender_pub_key>,
}
const fee: Fee = {
amount: '4000000000000000',
denom: 'aevmos',
gas: '200000',
}
const memo = ''
const context: TxContext = {
chain,
sender,
fee,
memo,
}
const params: MsgSendParams = {
destinationAddress: <destination_address>,
amount: <transaction_amount>,
denom: 'aevmos',
}
const tx: TxPayload = createTxMsgSend(context, params)
Sign the transaction
After creating the transaction, developers need to send the payload to the appropriate wallet to be signed.
To sign a payload using MetaMask, developers are encouraged to use the EIP-712 signing format, as this is compatible with the Ledger Ethereum app.
To sign a payload using Keplr with a Ledger device, developers must use the EIP-712 signing format, since the Ledger Ethereum app cannot sign native Cosmos payloads. Evmos uses different signing and address generation than that of the Cosmos Ledger app, which is thus incompatible.
To sign a payload using Keplr otherwise, developers may
use either the EIP-712
signing format or the native Cosmos SignDirect
format.
We encourage using the SignDirect
format, since it provides
a native signing experience.
Sign the transaction with MetaMask (EIP-712)
Evmos supports EIP-712 signatures to sign Cosmos transactions using Ethereum wallets such as MetaMask.
import { createTxRaw } from '@evmos/proto'
import { evmosToEth } from '@evmos/address-converter'
// First, populate a TxContext object and create a signable Tx payload.
// (See 'Create the Transaction' to learn how to create these).
const context = ...
const tx = ...
const { sender } = context
// Initialize MetaMask and sign the EIP-712 payload.
await window.ethereum.enable()
const senderHexAddress = evmosToEth(sender.accountAddress)
const eip712Payload = JSON.stringify(tx.eipToSign)
const signature = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [senderHexAddress, eip712Payload],
})
// Create a signed Tx payload that can be broadcast to a node.
const signatureBytes = Buffer.from(signature.replace('0x', ''), 'hex')
const { signDirect } = tx
const bodyBytes = signDirect.body.toBinary()
const authInfoBytes = signDirect.authInfo.toBinary()
const signedTx = createTxRaw(
bodyBytes,
authInfoBytes,
[signatureBytes],
)
Sign the Transaction with Keplr (SignDirect)
Sign a native Cosmos transaction using Keplr.
import { createTxRaw } from '@evmos/proto'
// First, populate a TxContext object and create a signable Tx payload.
// (See 'Create a Signable Transaction' to learn how to create these).
const context = ...
const tx = ...
const { chain, sender } = context
const { signDirect } = tx
const signResponse = await window?.keplr?.signDirect(
chain.cosmosChainId,
sender.accountAddress,
{
bodyBytes: signDirect.body.toBinary(),
authInfoBytes: signDirect.authInfo.toBinary(),
chainId: chain.cosmosChainId,
accountNumber: new Long(sender.accountNumber),
},
)
if (!signResponse) {
// Handle signature failure here.
}
const signatures = [
new Uint8Array(Buffer.from(signResponse.signature.signature, 'base64')),
]
const { signed } = signResponse
const signedTx = createTxRaw(
signed.bodyBytes,
signed.authInfoBytes,
signatures,
)
Sign the Transaction with Keplr (EIP-712)
Sign an EIP-712 payload using Keplr in order to support Ledger users on Keplr.
import { EthSignType } from '@keplr-wallet/types';
import { createTxRaw } from '@evmos/proto'
// First, populate a TxContext object and create a signable Tx payload.
// (See 'Create a Signable Transaction' to learn how to create these).
const context = ...
const tx = ...
const { chain, sender } = context
const eip712Payload = JSON.stringify(tx.eipToSign)
const signature = await window?.keplr?.signEthereum(
chain.cosmosChainId,
sender.accountAddress,
eip712Payload,
EthSignType.EIP712,
)
if (!signature) {
// Handle signature failure here.
}
const { signDirect } = tx
const bodyBytes = signDirect.body.toBinary()
const authInfoBytes = signDirect.authInfo.toBinary()
const signedTx = createTxRaw(
bodyBytes,
authInfoBytes,
[signature],
)
Broadcast the transaction
Broadcasting takes place the same way for all Cosmos transactions, regardless of how they are signed.
import {
generateEndpointBroadcast,
generatePostBodyBroadcast,
} from '@evmos/provider'
// First, sign a transaction using MetaMask or Keplr.
const signedTx = createTxRaw(...)
// Find a node URL from a network endpoint:
// https://docs.evmos.org/develop/api/networks.
const nodeUrl = ...
const postOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: generatePostBodyBroadcast(signedTx),
}
const broadcastEndpoint = `${nodeUrl}${generateEndpointBroadcast()}`
const broadcastPost = await fetch(
broadcastEndpoint,
postOptions,
)
const response = await broadcastPost.json()
Send Ethereum transactions
In addition to Cosmos transactions, developers can generate, sign, and broadcast Ethereum Virtual Machine (EVM) payloads. Both MetaMask and Keplr offer this functionality.
Sign EVM transaction using MetaMask
Signing EVM transactions using MetaMask follows the same procedure as on any other EVM chain. See MetaMask's documentation for more information.
const transactionParameters = {
nonce: '0x00', // ignored by MetaMask
gasPrice: '0x09184e72a000', // customizable by user during MetaMask confirmation.
gas: '0x2710', // customizable by user during MetaMask confirmation.
to: '0x0000000000000000000000000000000000000000', // Required except during contract publications.
from: ethereum.selectedAddress, // must match user's active address.
value: '0x00', // Only required to send ether to the recipient from the initiating external account.
data:
'0x7f7465737432000000000000000000000000000000000000000000000000000000600057', // Optional, but used for defining smart contract creation and interaction.
chainId: '0x3', // Used to prevent transaction reuse across blockchains. Auto-filled by MetaMask.
}
const txHash = await ethereum.request({
method: 'eth_sendTransaction',
params: [transactionParameters],
})
Sign EVM transaction using Keplr
Keplr supports signing EVM transactions from the wallet. These must be broadcast using a provider, such as that from ethersjs.
import { JsonRpcProvider } from '@ethersproject/providers'
import { evmosToEth } from '@evmos/address-converter'
// Find an Ethereum JSON-RPC node url for either mainnet or testnet here:
// https://docs.evmos.org/develop/api/networks.
const nodeUrl = '...'
const provider = new JsonRpcProvider(nodeUrl)
const chainId = 'evmos_9001-2';
await window.keplr.enable(chainId);
const offlineSigner = window.getOfflineSigner(chainId);
let wallets = await offlineSigner.getAccounts();
const signerAddressBech32 = wallets[0].address;
const signerAddress = evmosToEth(signerAddressBech32);
let rawTx = {
chainId: 9001, // Use 9000 for testnet
to: <destination_address>,
value: <value>,
data: <data>,
accessList: [],
type: 2, // Use type=2 to specify EIP-1559-style transaction
}
// Calculate and set nonce
const nonce = await provider.getTransactionCount(signerAddress)
rawTx['nonce'] = nonce
// Calculate and set gas fees
const gasLimit = await provider.estimateGas(rawTx)
const gasFee = await provider.getFeeData()
rawTx['gasLimit'] = gasLimit.toHexString()
if (!gasFee.maxPriorityFeePerGas || !gasFee.maxFeePerGas) {
// Handle error
return
}
rawTx['maxPriorityFeePerGas'] = gasFee.maxPriorityFeePerGas.toHexString();
rawTx['maxFeePerGas'] = gasFee.maxFeePerGas.toHexString();
const signedTx = await window.keplr.signEthereum(
chainId,
signerAddressBech32,
JSON.stringify(rawTx),
'transaction'
);
const res = await provider.sendTransaction(signedTx);
console.log(res);
// Result:
// {
// chainId: 1337,
// confirmations: 0,
// data: '0x',
// from: '0x8577181F3D8A38a532Ef8F3D6Fd9a31baE73b1EA',
// gasLimit: { BigNumber: "21000" },
// gasPrice: { BigNumber: "1" },
// hash: '0x200818a533113c00057ceccd3277249871c4a1ac09514214f03c3b96099b6c92',
// nonce: 4,
// r: '0x1727bd07080a5d3586422edad86805918e9772adda231d51c32870a1f1cabffb',
// s: '0x7afc6be528befb79b9ed250356f6eacd63e853685091e9a3987a3d266c6cb26a',
// to: '0x5555763613a12D8F3e73be831DFf8598089d3dCa',
// type: null,
// v: 2709,
// value: { BigNumber: "3141590000000000000" },
// wait: [Function]
// }