Skip to main content

Nethereum.Metamask

NuGet: Nethereum.Metamask | Source: src/Nethereum.Metamask/

Nethereum.Metamask

Core MetaMask integration abstractions for Nethereum. Provides platform-agnostic interfaces and request interceptors for integrating MetaMask wallet into .NET applications.

Overview

Nethereum.Metamask is the foundation package for MetaMask integration. It defines the core abstractions (IMetamaskInterop) and request interceptor logic needed to route Ethereum RPC calls through the MetaMask browser extension. Platform-specific implementations (Blazor, desktop, mobile) build on top of these abstractions.

Key Features:

  • IMetamaskInterop interface for platform-specific implementations
  • MetamaskHostProvider implementing IEthereumHostProvider
  • MetamaskInterceptor for routing RPC requests through MetaMask
  • Support for all MetaMask-specific methods (personal_sign, eth_signTypedData_v4, wallet_*)
  • Automatic account injection into transactions
  • Two operating modes: all requests or signing-only

Use Cases:

  • Building Blazor dApps with MetaMask
  • Creating custom MetaMask integrations for other platforms
  • Routing transaction signing through MetaMask while using custom RPC
  • Implementing wallet connection flows

Installation

dotnet add package Nethereum.Metamask

Note: This is the core abstraction package. For Blazor applications, use Nethereum.Metamask.Blazor which provides the JavaScript interop implementation.

Dependencies

  • Nethereum.UI
  • Nethereum.Web3

Architecture

Platform Implementations

┌─────────────────────┐  ┌──────────────────┐  ┌─────────────────┐
│ Nethereum.Metamask │ │ Nethereum.Unity │ │ Desktop/Mobile │
│ .Blazor │ │ .Metamask │ │ Implementations │
│ │ │ │ │ │
│ MetamaskBlazer- │ │ Unity-specific │ │ Platform- │
│ Interop (JS) │ │ implementation │ │ specific impl │
└─────────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┴──────────────────────┘

implements IMetamaskInterop


┌─────────────────────────────────────────────────────────────┐
│ Nethereum.Metamask (This Package) │
│ Core Abstractions │
│ │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ IMetamaskInterop │ │ MetamaskHostProvider │ │
│ │ (Platform Interface) │ │ (Wallet Provider) │ │
│ └──────────────────────┘ └────────────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ MetamaskInterceptor │ │ MetamaskRpcRequest- │ │
│ │ (Request Router) │ │ Message │ │
│ └──────────────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Nethereum.Web3 │
│ (Ethereum Interaction) │
└─────────────────────────────────────────────────────────────┘

IMetamaskInterop

Platform-specific interface that must be implemented by each platform (Blazor, Unity, desktop):

public interface IMetamaskInterop
{
Task<string> EnableEthereumAsync();
Task<bool> CheckMetamaskAvailability();
Task<string> GetSelectedAddress();
Task<RpcResponseMessage> SendAsync(RpcRequestMessage rpcRequestMessage);
Task<RpcResponseMessage> SendTransactionAsync(MetamaskRpcRequestMessage rpcRequestMessage);
Task<string> SignAsync(string utf8Hex);
}

MetamaskHostProvider

Implements IEthereumHostProvider for MetaMask:

public class MetamaskHostProvider : IEthereumHostProvider
{
public string Name { get; } // "Metamask"
public bool Available { get; }
public string SelectedAccount { get; }
public long SelectedNetworkChainId { get; }
public bool Enabled { get; }

// Events
event Func<string, Task> SelectedAccountChanged;
event Func<long, Task> NetworkChanged;
event Func<bool, Task> AvailabilityChanged;
event Func<bool, Task> EnabledChanged;

// Methods
Task<bool> CheckProviderAvailabilityAsync();
Task<string> EnableProviderAsync();
Task<IWeb3> GetWeb3Async();
Task<string> SignMessageAsync(string message);
}

MetamaskInterceptor

Intercepts Web3 requests and routes appropriate methods through MetaMask:

public class MetamaskInterceptor : RequestInterceptor
{
// Methods routed through MetaMask
public static List<string> SigningWalletTransactionsMethods { get; } = new List<string>
{
"eth_sendTransaction",
"eth_signTransaction",
"eth_sign",
"personal_sign",
"eth_signTypedData",
"eth_signTypedData_v3",
"eth_signTypedData_v4",
"wallet_watchAsset",
"wallet_addEthereumChain",
"wallet_switchEthereumChain"
};
}

Usage Examples

Example 1: Basic Setup (Requires Platform Implementation)

using Nethereum.Metamask;
using Nethereum.UI;

// Platform-specific interop implementation
// In Blazor: MetamaskBlazorInterop
// In other platforms: implement IMetamaskInterop
IMetamaskInterop metamaskInterop = GetPlatformSpecificInterop();

// Create MetaMask provider
var metamaskProvider = new MetamaskHostProvider(metamaskInterop);

// Check if MetaMask is available
bool isAvailable = await metamaskProvider.CheckProviderAvailabilityAsync();
if (!isAvailable)
{
Console.WriteLine("MetaMask extension not detected");
return;
}

// Request connection
string account = await metamaskProvider.EnableProviderAsync();
Console.WriteLine($"Connected to MetaMask: {account}");

// Get Web3 instance
var web3 = await metamaskProvider.GetWeb3Async();

// All transactions will be signed by MetaMask
var balance = await web3.Eth.GetBalance.SendRequestAsync(account);
Console.WriteLine($"Balance: {Web3.Convert.FromWei(balance)} ETH");

Example 2: Listening to Account Changes

using Nethereum.Metamask;

var metamaskProvider = new MetamaskHostProvider(metamaskInterop);

// Subscribe to account changes
metamaskProvider.SelectedAccountChanged += async (newAccount) =>
{
Console.WriteLine($"Account changed to: {newAccount}");

// Update UI, reload balances, etc.
var web3 = await metamaskProvider.GetWeb3Async();
var balance = await web3.Eth.GetBalance.SendRequestAsync(newAccount);
Console.WriteLine($"New balance: {Web3.Convert.FromWei(balance)} ETH");
};

// Subscribe to network changes
metamaskProvider.NetworkChanged += async (chainId) =>
{
Console.WriteLine($"Network changed to chain ID: {chainId}");

if (chainId == 1)
Console.WriteLine("Connected to Ethereum Mainnet");
else if (chainId == 137)
Console.WriteLine("Connected to Polygon");
else
Console.WriteLine($"Connected to unknown network: {chainId}");
};

// Enable provider
await metamaskProvider.EnableProviderAsync();

Example 3: Sending Transactions Through MetaMask

using Nethereum.Metamask;
using Nethereum.Web3;
using Nethereum.Hex.HexTypes;

var metamaskProvider = new MetamaskHostProvider(metamaskInterop);
await metamaskProvider.EnableProviderAsync();

var web3 = await metamaskProvider.GetWeb3Async();

// Send ETH (MetaMask will prompt for signature)
var toAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb";
var amountInWei = Web3.Convert.ToWei(0.1);

var txHash = await web3.Eth.GetEtherTransferService()
.TransferEtherAndWaitForReceiptAsync(toAddress, 0.1m);

Console.WriteLine($"Transaction sent: {txHash}");

Example 4: Signing Messages with MetaMask

using Nethereum.Metamask;
using Nethereum.Signer;

var metamaskProvider = new MetamaskHostProvider(metamaskInterop);
await metamaskProvider.EnableProviderAsync();

// Sign message (MetaMask popup will appear)
string message = "Sign this message to authenticate";
string signature = await metamaskProvider.SignMessageAsync(message);

Console.WriteLine($"Signature: {signature}");

// Verify signature
var signer = new EthereumMessageSigner();
var recoveredAddress = signer.EncodeUTF8AndEcRecover(message, signature);

Console.WriteLine($"Recovered address: {recoveredAddress}");
Console.WriteLine($"Matches selected account: {recoveredAddress.Equals(metamaskProvider.SelectedAccount, StringComparison.OrdinalIgnoreCase)}");

Example 5: Hybrid Mode - MetaMask for Signing, Custom RPC for Queries

using Nethereum.Metamask;
using Nethereum.JsonRpc.Client;
using Nethereum.Web3;

// Custom RPC client for queries (faster, no MetaMask popups)
var customRpcClient = new RpcClient(new Uri("https://mainnet.infura.io/v3/YOUR-PROJECT-ID"));

// MetaMask provider with custom RPC client
// useOnlySigningWalletTransactionMethods = true means:
// - Queries go through custom RPC
// - Signatures go through MetaMask
var metamaskProvider = new MetamaskHostProvider(
metamaskInterop,
client: customRpcClient,
useOnlySigningWalletTransactionMethods: true);

await metamaskProvider.EnableProviderAsync();

var web3 = await metamaskProvider.GetWeb3Async();

// This query goes through Infura (fast, no MetaMask popup)
var blockNumber = await web3.Eth.Blocks.GetBlockNumber.SendRequestAsync();
Console.WriteLine($"Current block: {blockNumber.Value}");

// This transaction goes through MetaMask (user signature required)
var txHash = await web3.Eth.GetEtherTransferService()
.TransferEtherAsync("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", 0.01m);

Console.WriteLine($"Transaction sent via MetaMask: {txHash}");

Example 6: Typed Data Signing (EIP-712)

using Nethereum.Metamask;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Signer.EIP712;

var metamaskProvider = new MetamaskHostProvider(metamaskInterop);
await metamaskProvider.EnableProviderAsync();

var web3 = await metamaskProvider.GetWeb3Async();

// Define typed data
var typedData = new
{
types = new
{
EIP712Domain = new[]
{
new { name = "name", type = "string" },
new { name = "version", type = "string" },
new { name = "chainId", type = "uint256" }
},
Person = new[]
{
new { name = "name", type = "string" },
new { name = "wallet", type = "address" }
}
},
primaryType = "Person",
domain = new
{
name = "MyDApp",
version = "1",
chainId = 1
},
message = new
{
name = "Alice",
wallet = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
}
};

// Sign typed data (MetaMask will show structured data)
var signature = await web3.Eth.AccountSigning.SignTypedDataV4.SendRequestAsync(
System.Text.Json.JsonSerializer.Serialize(typedData));

Console.WriteLine($"EIP-712 Signature: {signature}");

Example 7: Adding Token to MetaMask (wallet_watchAsset)

using Nethereum.Metamask;
using Nethereum.Web3;

var metamaskProvider = new MetamaskHostProvider(metamaskInterop);
await metamaskProvider.EnableProviderAsync();

var web3 = await metamaskProvider.GetWeb3Async();

// Prepare token details
var tokenDetails = new
{
type = "ERC20",
options = new
{
address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
symbol = "USDC",
decimals = 6,
image = "https://example.com/usdc.png"
}
};

// Request to add token to MetaMask
// Note: This requires using the interceptor
var result = await web3.Client.SendRequestAsync<bool>(
"wallet_watchAsset",
null,
tokenDetails);

if (result)
{
Console.WriteLine("Token added to MetaMask successfully");
}

Example 8: Switching Networks in MetaMask

using Nethereum.Metamask;
using Nethereum.Web3;
using Nethereum.Hex.HexTypes;

var metamaskProvider = new MetamaskHostProvider(metamaskInterop);
await metamaskProvider.EnableProviderAsync();

var web3 = await metamaskProvider.GetWeb3Async();

// Switch to Polygon network
var switchParams = new
{
chainId = "0x89" // Polygon chain ID in hex (137 in decimal)
};

try
{
await web3.Client.SendRequestAsync<object>(
"wallet_switchEthereumChain",
null,
new[] { switchParams });

Console.WriteLine("Switched to Polygon network");
}
catch (RpcResponseException ex) when (ex.RpcError.Code == 4902)
{
// Network not added to MetaMask, add it first
var addChainParams = new
{
chainId = "0x89",
chainName = "Polygon Mainnet",
nativeCurrency = new
{
name = "MATIC",
symbol = "MATIC",
decimals = 18
},
rpcUrls = new[] { "https://polygon-rpc.com/" },
blockExplorerUrls = new[] { "https://polygonscan.com/" }
};

await web3.Client.SendRequestAsync<object>(
"wallet_addEthereumChain",
null,
new[] { addChainParams });

Console.WriteLine("Added and switched to Polygon network");
}

API Reference

MetamaskHostProvider

public class MetamaskHostProvider : IEthereumHostProvider
{
// Constructor
public MetamaskHostProvider(
IMetamaskInterop metamaskInterop,
IClient client = null,
bool useOnlySigningWalletTransactionMethods = false);

// Properties
public static MetamaskHostProvider Current { get; }
public string Name { get; }
public bool Available { get; }
public string SelectedAccount { get; }
public long SelectedNetworkChainId { get; }
public bool Enabled { get; }
public IClient Client { get; }

// Events
public event Func<string, Task> SelectedAccountChanged;
public event Func<long, Task> NetworkChanged;
public event Func<bool, Task> AvailabilityChanged;
public event Func<bool, Task> EnabledChanged;

// Methods
public Task<bool> CheckProviderAvailabilityAsync();
public Task<string> EnableProviderAsync();
public Task<IWeb3> GetWeb3Async();
public Task<string> GetProviderSelectedAccountAsync();
public Task<string> SignMessageAsync(string message);

// State change methods (called by platform layer)
public Task ChangeSelectedAccountAsync(string selectedAccount);
public Task ChangeSelectedNetworkAsync(long chainId);
public Task ChangeMetamaskAvailableAsync(bool available);
public Task ChangeMetamaskEnabledAsync(bool enabled);
}

IMetamaskInterop

public interface IMetamaskInterop
{
Task<string> EnableEthereumAsync();
Task<bool> CheckMetamaskAvailability();
Task<string> GetSelectedAddress();
Task<RpcResponseMessage> SendAsync(RpcRequestMessage rpcRequestMessage);
Task<RpcResponseMessage> SendTransactionAsync(MetamaskRpcRequestMessage rpcRequestMessage);
Task<string> SignAsync(string utf8Hex);
}

MetamaskInterceptor

public class MetamaskInterceptor : RequestInterceptor
{
public MetamaskInterceptor(
IMetamaskInterop metamaskInterop,
bool useOnlySigningWalletTransactionMethods = false);

public static List<string> SigningWalletTransactionsMethods { get; }
public string SelectedAccount { get; set; }
}

MetamaskRpcRequestMessage

public class MetamaskRpcRequestMessage : RpcRequestMessage
{
public MetamaskRpcRequestMessage(
object id,
string method,
string from,
params object[] parameterList);

public string From { get; }
}

Important Notes

Platform-Specific Implementation Required

This package provides abstractions only. You must implement IMetamaskInterop for your platform:

  • Blazor: Use Nethereum.Metamask.Blazor (provides JavaScript interop)
  • Desktop/Mobile: Implement IMetamaskInterop with embedded browser control
  • Unity: Implement IMetamaskInterop with WebGL or mobile wallet integration

Two Operating Modes

// Mode 1: All requests through MetaMask (default)
var provider = new MetamaskHostProvider(interop);
// Queries and transactions both use MetaMask
// Slower but everything goes through wallet

// Mode 2: Signing only through MetaMask (hybrid)
var provider = new MetamaskHostProvider(
interop,
client: customRpcClient,
useOnlySigningWalletTransactionMethods: true);
// Queries use custom RPC (faster)
// Transactions use MetaMask (secure)

Automatically Handled Methods

The MetamaskInterceptor automatically routes these methods through MetaMask:

  • eth_sendTransaction - Sends transactions (user approval required)
  • eth_signTransaction - Signs transactions without sending
  • eth_sign - Personal message signing
  • personal_sign - EIP-191 personal message signing
  • eth_signTypedData - Typed data signing (EIP-712)
  • eth_signTypedData_v3 - Typed data signing v3
  • eth_signTypedData_v4 - Typed data signing v4 (recommended)
  • wallet_watchAsset - Add token to MetaMask
  • wallet_addEthereumChain - Add custom network
  • wallet_switchEthereumChain - Switch network

All other methods (queries like eth_call, eth_getBalance, etc.) use the configured RPC client.

Account Injection

The interceptor automatically injects the selected MetaMask account into transactions:

var web3 = await provider.GetWeb3Async();

// No need to specify 'from' - automatically uses MetaMask account
await web3.Eth.GetEtherTransferService()
.TransferEtherAsync(toAddress, amount);

Static Current Property

For convenience, the last created MetamaskHostProvider is available via:

var provider = new MetamaskHostProvider(interop);
// Later, anywhere in code:
var currentProvider = MetamaskHostProvider.Current;

Error Handling

MetaMask interactions can fail for various reasons:

try
{
var txHash = await web3.Eth.GetEtherTransferService()
.TransferEtherAsync(toAddress, amount);
}
catch (RpcResponseException ex)
{
// User rejected transaction
if (ex.RpcError.Code == 4001)
{
Console.WriteLine("User rejected the transaction");
}
// Insufficient funds
else if (ex.RpcError.Message.Contains("insufficient funds"))
{
Console.WriteLine("Insufficient funds");
}
else
{
Console.WriteLine($"Transaction failed: {ex.Message}");
}
}

ValueTask Support

On .NET Core 3.1+, IMetamaskInterop uses ValueTask<T> for better performance. On earlier frameworks, it uses Task<T>.

Platform Implementations

  • Nethereum.Metamask.Blazor - Blazor WebAssembly implementation with JavaScript interop

Dependencies

  • Nethereum.UI - Ethereum host provider abstractions
  • Nethereum.Web3 - Web3 client and RPC functionality
  • Nethereum.Blazor - Blazor components and services
  • Nethereum.EIP6963WalletInterop - Multi-wallet discovery
  • Nethereum.WalletConnect - WalletConnect integration
  • Nethereum.Reown.AppKit.Blazor - Reown AppKit integration

Additional Resources