Skip to main content
Send transactions from a user’s embedded Ethereum wallet to interact with smart contracts, transfer ETH, or execute any onchain action.

Overview

The useSendTransaction hook from MoonKey’s React SDK provides a simple interface for sending transactions from embedded wallets. It handles transaction preparation, signing, and broadcasting to the network.
MoonKey automatically handles transaction population (gas estimation, nonce management, etc.) and prompts users for confirmation before broadcasting.

React SDK

  • Basic Usage
  • With UI Customization
To send a transaction, use the sendTransaction method from the useSendTransaction hook:
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/ethereum';

function SendEthButton() {
  const { user } = useMoonKey();
  const { sendTransaction } = useSendTransaction();

  const handleSend = async () => {
    if (!user?.wallet) {
      alert('No wallet found');
      return;
    }

    try {
      const result = await sendTransaction({
        to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
        value: '0x38d7ea4c68000' // 0.001 ETH in wei (hex)
      });

      console.log('Transaction sent:', result.transactionHash);
      alert(`Success! Hash: ${result.transactionHash}`);
    } catch (error) {
      console.error('Transaction failed:', error);
      alert('Transaction failed');
    }
  };

  return (
    <button onClick={handleSend}>
      Send 0.001 ETH
    </button>
  );
}

Parameters

Transaction Object

The first parameter to sendTransaction is the transaction object:
to
string
required
The recipient address for the transaction.Example:
to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
value
string
The amount of ETH to send in wei, as a hexadecimal string. Omit for contract interactions with no ETH transfer.Example:
value: '0x38d7ea4c68000' // 0.001 ETH
data
string
The encoded function call data for contract interactions, as a hexadecimal string.Example:
data: '0x095ea7b3000000000000000000000000...'
chainId
number
The chain ID for the transaction. If not provided, uses the wallet’s current chain.Example:
chainId: 8453 // Base
gasLimit
string
Optional gas limit as a hexadecimal string. MoonKey estimates gas automatically if not provided.Example:
gasLimit: '0x5208' // 21000 in decimal
maxFeePerGas
string
Optional maximum fee per gas for EIP-1559 transactions, as a hexadecimal string.Example:
maxFeePerGas: '0x59682f00' // 1.5 gwei
maxPriorityFeePerGas
string
Optional maximum priority fee per gas for EIP-1559 transactions, as a hexadecimal string.Example:
maxPriorityFeePerGas: '0x3b9aca00' // 1 gwei
nonce
number
Optional nonce for the transaction. MoonKey determines this automatically if not provided.Example:
nonce: 5

Options Object

The second parameter is an optional configuration object:
uiConfig
object
Configuration for the transaction confirmation modal UI.
wallet
Wallet
Specific wallet to use for the transaction. If not provided, uses the user’s default wallet.Example:
wallet: user.wallet

Returns

The sendTransaction method returns a Promise that resolves with:
transactionHash
string
The hash of the broadcasted transaction.Example:
"0x7c91ba85d67ef92cc15f3e9c8d8c5788e982cf83fabe9bfcc66a747aa0bd3701"
chainId
number
The chain ID where the transaction was sent.
A successful return indicates the transaction was broadcasted to the network. It may still fail during confirmation. Always verify transaction status onchain or listen for confirmation events.

Complete Examples

Basic ETH Transfer

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/ethereum';
import { useState } from 'react';

export default function SendEthComponent() {
  const { user, isAuthenticated } = useMoonKey();
  const { sendTransaction } = useSendTransaction();
  const [recipientAddress, setRecipientAddress] = useState('');
  const [amount, setAmount] = useState('');
  const [txHash, setTxHash] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [isSending, setIsSending] = useState(false);

  const handleSend = async () => {
    if (!user?.wallet) {
      setError('No wallet found');
      return;
    }

    if (!recipientAddress || !amount) {
      setError('Please fill in all fields');
      return;
    }

    setError(null);
    setTxHash(null);
    setIsSending(true);

    try {
      // Convert ETH amount to wei (hex)
      const amountInWei = BigInt(parseFloat(amount) * 10**18);
      const valueHex = '0x' + amountInWei.toString(16);

      const result = await sendTransaction({
        to: recipientAddress,
        value: valueHex
      });

      setTxHash(result.transactionHash);
      alert('Transaction sent successfully!');
    } catch (err: any) {
      console.error('Transaction failed:', err);
      setError(err.message || 'Transaction failed');
    } finally {
      setIsSending(false);
    }
  };

  if (!isAuthenticated || !user?.wallet) {
    return <p>Please sign in and create a wallet</p>;
  }

  return (
    <div className="send-eth-container">
      <h2>Send ETH</h2>

      <div className="form-group">
        <label>Recipient Address</label>
        <input
          type="text"
          value={recipientAddress}
          onChange={(e) => setRecipientAddress(e.target.value)}
          placeholder="0x..."
          disabled={isSending}
        />
      </div>

      <div className="form-group">
        <label>Amount (ETH)</label>
        <input
          type="number"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          placeholder="0.001"
          step="0.001"
          disabled={isSending}
        />
      </div>

      <button
        onClick={handleSend}
        disabled={isSending || !recipientAddress || !amount}
        className="send-button"
      >
        {isSending ? 'Sending...' : 'Send ETH'}
      </button>

      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      {txHash && (
        <div className="success-message">
          <p>Transaction sent!</p>
          <p className="tx-hash">Hash: {txHash}</p>
          <a
            href={`https://etherscan.io/tx/${txHash}`}
            target="_blank"
            rel="noopener noreferrer"
          >
            View on Etherscan
          </a>
        </div>
      )}
    </div>
  );
}

ERC-20 Token Transfer

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/ethereum';
import { encodeFunctionData } from 'viem';
import { useState } from 'react';

// ERC-20 ABI for transfer function
const ERC20_ABI = [{
  name: 'transfer',
  type: 'function',
  inputs: [
    { name: 'to', type: 'address' },
    { name: 'amount', type: 'uint256' }
  ],
  outputs: [{ type: 'bool' }]
}];

export default function SendTokenComponent() {
  const { user } = useMoonKey();
  const { sendTransaction } = useSendTransaction();
  const [tokenAddress, setTokenAddress] = useState('');
  const [recipientAddress, setRecipientAddress] = useState('');
  const [amount, setAmount] = useState('');
  const [decimals, setDecimals] = useState('18');
  const [isSending, setIsSending] = useState(false);

  const handleSendToken = async () => {
    if (!user?.wallet) return;

    setIsSending(true);

    try {
      // Convert token amount to smallest unit
      const amountInSmallestUnit = BigInt(
        parseFloat(amount) * 10**parseInt(decimals)
      );

      // Encode the transfer function call
      const data = encodeFunctionData({
        abi: ERC20_ABI,
        functionName: 'transfer',
        args: [recipientAddress, amountInSmallestUnit]
      });

      const result = await sendTransaction({
        to: tokenAddress,
        data
      }, {
        uiConfig: {
          title: 'Send Token',
          description: `Sending ${amount} tokens to ${recipientAddress.slice(0, 6)}...${recipientAddress.slice(-4)}`,
          confirmButtonText: 'Confirm Transfer'
        }
      });

      console.log('Token transfer successful:', result.transactionHash);
      alert('Token transfer successful!');
    } catch (error) {
      console.error('Token transfer failed:', error);
      alert('Token transfer failed');
    } finally {
      setIsSending(false);
    }
  };

  return (
    <div className="send-token-container">
      <h2>Send ERC-20 Token</h2>

      <div className="form-group">
        <label>Token Contract Address</label>
        <input
          type="text"
          value={tokenAddress}
          onChange={(e) => setTokenAddress(e.target.value)}
          placeholder="0x..."
        />
      </div>

      <div className="form-group">
        <label>Recipient Address</label>
        <input
          type="text"
          value={recipientAddress}
          onChange={(e) => setRecipientAddress(e.target.value)}
          placeholder="0x..."
        />
      </div>

      <div className="form-group">
        <label>Amount</label>
        <input
          type="number"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          placeholder="100"
          step="0.01"
        />
      </div>

      <div className="form-group">
        <label>Token Decimals</label>
        <input
          type="number"
          value={decimals}
          onChange={(e) => setDecimals(e.target.value)}
          placeholder="18"
        />
      </div>

      <button
        onClick={handleSendToken}
        disabled={isSending || !tokenAddress || !recipientAddress || !amount}
      >
        {isSending ? 'Sending...' : 'Send Token'}
      </button>
    </div>
  );
}

UI Customization

Customize the transaction confirmation modal to match your application’s branding:
import { useSendTransaction } from '@moon-key/react-auth/ethereum';

const { sendTransaction } = useSendTransaction();

await sendTransaction({
  to: '0x...',
  value: '0x38d7ea4c68000'
}, {
  uiConfig: {
    title: 'Confirm Payment',
    description: 'You are about to send 0.001 ETH',
    confirmButtonText: 'Send Payment',
    cancelButtonText: 'Cancel',
    transactionInfo: {
      amount: '0.001 ETH',
      recipient: 'vitalik.eth',
      fee: '~$0.50'
    }
  }
});

Hide Modal

For certain flows, you may want to send transactions without showing the UI modal:
await sendTransaction({
  to: '0x...',
  value: '0x38d7ea4c68000'
}, {
  uiConfig: {
    showWalletUI: false
  }
});
Hiding the confirmation modal removes the user’s ability to review transaction details before signing. Only use this for trusted operations or after obtaining explicit user consent.

Working with Viem

MoonKey’s embedded wallets are compatible with viem, allowing you to leverage its powerful utilities:
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/ethereum';
import { parseEther, encodeFunctionData } from 'viem';

function ViemExample() {
  const { user } = useMoonKey();
  const { sendTransaction } = useSendTransaction();

  const sendWithViem = async () => {
    if (!user?.wallet) return;

    // Use viem utilities
    const value = parseEther('0.001'); // Returns bigint

    const result = await sendTransaction({
      to: '0xRecipient...',
      value: '0x' + value.toString(16) // Convert to hex
    });

    console.log('Transaction sent:', result.transactionHash);
  };

  return <button onClick={sendWithViem}>Send with Viem</button>;
}

Error Handling

Handle common transaction errors gracefully:
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/ethereum';

function TransactionWithErrorHandling() {
  const { user } = useMoonKey();
  const { sendTransaction } = useSendTransaction();

  const handleTransaction = async () => {
    if (!user?.wallet) return;

    try {
      const result = await sendTransaction({
        to: '0x...',
        value: '0x38d7ea4c68000'
      });

      console.log('Success:', result.transactionHash);
    } catch (error: any) {
      // Handle specific error types
      if (error.code === 'USER_REJECTED') {
        console.log('User cancelled the transaction');
        alert('Transaction cancelled');
      } else if (error.code === 'INSUFFICIENT_FUNDS') {
        console.log('Insufficient balance');
        alert('Not enough ETH for this transaction');
      } else if (error.code === 'NETWORK_ERROR') {
        console.log('Network error');
        alert('Network error. Please try again');
      } else {
        console.error('Transaction failed:', error);
        alert('Transaction failed: ' + error.message);
      }
    }
  };

  return <button onClick={handleTransaction}>Send Transaction</button>;
}

Best Practices

Validate addresses and amounts before sending transactions:
import { isAddress } from 'viem';

const handleSend = async () => {
  // Validate address
  if (!isAddress(recipientAddress)) {
    alert('Invalid recipient address');
    return;
  }

  // Validate amount
  if (parseFloat(amount) <= 0) {
    alert('Amount must be greater than 0');
    return;
  }

  // Proceed with transaction
  await sendTransaction({ to: recipientAddress, value: amountInWei });
};
Verify sufficient balance before attempting to send:
import { useWalletBalance } from '@moon-key/react-auth/ethereum';

const { balance } = useWalletBalance();

const handleSend = async () => {
  const amountInWei = BigInt(parseFloat(amount) * 10**18);

  if (balance && BigInt(balance) < amountInWei) {
    alert('Insufficient balance');
    return;
  }

  await sendTransaction({ to: recipient, value: '0x' + amountInWei.toString(16) });
};
Keep users informed throughout the transaction process:
const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');

const handleSend = async () => {
  setStatus('pending');

  try {
    const result = await sendTransaction({ to: '0x...', value: '0x38d7ea4c68000' });
    setStatus('success');
    showToast('Transaction sent! Hash: ' + result.transactionHash);
  } catch (error) {
    setStatus('error');
    showToast('Transaction failed');
  }
};
Let MoonKey handle gas estimation, but allow overrides when needed:
// MoonKey handles gas estimation automatically
await sendTransaction({
  to: '0x...',
  value: '0x38d7ea4c68000'
  // gasLimit is optional - only provide if you need a specific value
});

// Or provide custom gas parameters for advanced use cases
await sendTransaction({
  to: '0x...',
  value: '0x38d7ea4c68000',
  gasLimit: '0x5208',
  maxFeePerGas: '0x59682f00',
  maxPriorityFeePerGas: '0x3b9aca00'
});
Wait for transaction confirmations before considering it final:
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http()
});

const handleSend = async () => {
  const result = await sendTransaction({ to: '0x...', value: '0x38d7ea4c68000' });

  console.log('Transaction broadcasted:', result.transactionHash);

  // Wait for confirmation
  const receipt = await publicClient.waitForTransactionReceipt({
    hash: result.transactionHash as `0x${string}`
  });

  console.log('Transaction confirmed:', receipt.status);
};

Troubleshooting

If transactions fail without clear errors:
  • Check that the user’s wallet has sufficient balance
  • Verify the recipient address is valid
  • Ensure the contract address is correct for contract interactions
  • Check that the encoded data is properly formatted
  • Try on a testnet first to debug
// Add detailed logging
try {
  console.log('Sending transaction:', { to, value, data });
  const result = await sendTransaction({ to, value, data });
  console.log('Transaction sent:', result);
} catch (error) {
  console.error('Full error:', error);
  console.error('Error details:', JSON.stringify(error, null, 2));
}
Handle user cancellations gracefully:
catch (error: any) {
  if (error.code === 'USER_REJECTED' || error.message.includes('User rejected')) {
    console.log('User cancelled the transaction');
    // Don't show error - this is expected behavior
    return;
  }
  // Handle other errors
  alert('Transaction failed: ' + error.message);
}
If transactions fail due to gas issues:
  • MoonKey usually handles gas estimation automatically
  • For complex transactions, consider providing a custom gas limit
  • Ensure the wallet has enough ETH for both value + gas
// Check if error is gas-related
if (error.message.includes('gas') || error.message.includes('insufficient funds')) {
  alert('Insufficient ETH for gas. Please add more ETH to your wallet.');
}
If a transaction appears stuck:
  • Check the network status (congestion, downtime)
  • Verify the transaction on a block explorer
  • Consider the gas price used - low gas prices can cause delays
  • Wait for network confirmation (can take minutes during congestion)
// Implement a timeout
const TIMEOUT = 60000; // 60 seconds

const sendWithTimeout = async () => {
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Transaction timeout')), TIMEOUT)
  );

  try {
    const result = await Promise.race([
      sendTransaction({ to: '0x...', value: '0x38d7ea4c68000' }),
      timeoutPromise
    ]);
    console.log('Transaction sent:', result);
  } catch (error: any) {
    if (error.message === 'Transaction timeout') {
      alert('Transaction is taking longer than expected. Check your wallet or explorer.');
    }
  }
};
Ensure transactions are sent on the correct network:
import { useWalletChain } from '@moon-key/react-auth/ethereum';

const { chainId, switchChain } = useWalletChain();

const handleSend = async () => {
  const targetChainId = 8453; // Base

  if (chainId !== targetChainId) {
    try {
      await switchChain(targetChainId);
    } catch (error) {
      alert('Please switch to Base network');
      return;
    }
  }

  await sendTransaction({ to: '0x...', value: '0x38d7ea4c68000' });
};

Next Steps