| name | send-evm-transaction |
| description | Send ETH and contract transactions with MetaMask using eth_sendTransaction via the EIP-1193 provider, gas estimation, receipt polling, and the connectWith shortcut |
Send EVM Transactions with MetaMask Connect
When to use
Use this skill when:
- Sending ETH transfers via
eth_sendTransaction
- Calling smart contract functions by encoding
data in the transaction
- Estimating gas with
eth_estimateGas before sending
- Polling for transaction confirmation with
eth_getTransactionReceipt
- Using the
connectWith shortcut to connect and send in a single approval
Workflow
Step 1: Get the provider and connected account
import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm';
import type { Hex, Address } from '@metamask/connect-evm';
const client = await createEVMClient({
dapp: { name: 'My DApp', url: window.location.href },
api: {
supportedNetworks: {
...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89'] }),
},
},
});
const { accounts } = await client.connect({ chainIds: ['0x1'] });
const provider = client.getProvider();
const from = accounts[0] as Address;
Step 2: Convert ETH to hex wei
All value fields in transactions must be hex-encoded wei. 1 ETH = 10^18 wei:
function ethToHexWei(ethAmount: string): Hex {
const wei = BigInt(Math.round(parseFloat(ethAmount) * 1e18));
return `0x${wei.toString(16)}` as Hex;
}
ethToHexWei('0.01');
ethToHexWei('0.001');
ethToHexWei('1');
Step 3: Send an ETH transfer
Build the transaction params and call eth_sendTransaction:
const txParams = {
from: from,
to: '0xRecipientAddress' as Address,
value: ethToHexWei('0.01'),
};
try {
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [txParams],
}) as Hex;
console.log('Transaction hash:', txHash);
} catch (err: any) {
if (err.code === 4001) {
console.log('User rejected the transaction');
return;
}
if (err.code === -32002) {
console.log('A transaction request is already pending');
return;
}
throw err;
}
Step 4: Estimate gas before sending
Use eth_estimateGas to get a gas estimate, then optionally add a buffer:
const txParams = {
from: from,
to: '0xRecipientAddress' as Address,
value: ethToHexWei('0.01'),
data: '0x',
};
const estimatedGas = await provider.request({
method: 'eth_estimateGas',
params: [txParams],
}) as Hex;
const gasWithBuffer = BigInt(estimatedGas) * 120n / 100n;
const gasHex = `0x${gasWithBuffer.toString(16)}` as Hex;
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [{
...txParams,
gas: gasHex,
}],
}) as Hex;
Step 5: Send a contract interaction
Encode the function call as the data field. For an ERC-20 transfer(address,uint256):
const recipient = '0xRecipientAddress'.slice(2).padStart(64, '0');
const amount = (1000000).toString(16).padStart(64, '0');
const data = `0xa9059cbb${recipient}${amount}` as Hex;
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [{
from: from,
to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Address,
data: data,
value: '0x0',
}],
}) as Hex;
Step 6: Poll for transaction receipt
After sending, poll eth_getTransactionReceipt until the transaction is confirmed:
async function waitForReceipt(
provider: any,
txHash: Hex,
intervalMs = 2000,
timeoutMs = 120000,
): Promise<any> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const receipt = await provider.request({
method: 'eth_getTransactionReceipt',
params: [txHash],
});
if (receipt !== null) {
return receipt;
}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`Transaction ${txHash} not confirmed within ${timeoutMs}ms`);
}
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [txParams],
}) as Hex;
const receipt = await waitForReceipt(provider, txHash);
if (receipt.status === '0x1') {
console.log('Transaction confirmed in block:', parseInt(receipt.blockNumber, 16));
} else {
console.error('Transaction reverted');
}
Step 7: Use connectWith for single-approval flow
connectWith connects the wallet and sends a transaction in one user interaction:
const { accounts, chainId, result } = await client.connectWith({
method: 'eth_sendTransaction',
params: (account: Address) => [
{
from: account,
to: '0xRecipientAddress' as Address,
value: ethToHexWei('0.01'),
},
],
chainIds: ['0x1'],
});
const txHash = result as Hex;
console.log('Connected as:', accounts[0]);
console.log('Transaction hash:', txHash);
The params field accepts a function that receives the first connected account ((account: Address) => unknown[]), letting you use the connected address as from without knowing it ahead of time.
Step 8: Handle errors
try {
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [txParams],
});
} catch (err: any) {
switch (err.code) {
case 4001:
break;
case -32002:
break;
case -32000:
console.error('Execution error:', err.message);
break;
default:
console.error('Transaction failed:', err);
}
}
Important Notes
value must be hex-encoded wei — '0xde0b6b3a7640000' is 1 ETH. Never pass decimal strings or ETH-denominated numbers directly.
from must match the connected account — MetaMask rejects transactions where from doesn't match the active account.
eth_sendTransaction returns a transaction hash, not a receipt — poll eth_getTransactionReceipt to confirm the transaction was mined.
- Receipt
status is hex — '0x1' means success, '0x0' means the transaction was mined but reverted.
eth_estimateGas can throw — if the transaction would revert, estimation fails. Wrap it in a try/catch and show the error to the user.
connectWith params can be a function — params: (account) => [{ from: account, ... }] — it receives the first connected account (a single Address, not an array).
- Chain IDs are always hex strings in SDK calls —
'0x1', '0x89', '0xaa36a7'. The chainId in transaction objects follows the same convention when present.
- Error code 4001 means the user deliberately rejected — handle gracefully.
- Error code -32002 means a request is pending — do not send another transaction.
0x1 is auto-included in every connect() / connectWith() call.