Skip to main content

Dapp frontend

Connecting to a wallet

MetaMask and other browser wallets inject an EIP-1193 provider at window.ethereum.

if (window.ethereum) {
const provider = window.ethereum;
try {
await provider.request({ method: "eth_requestAccounts" });
} catch (err) {
console.error("User denied account access", err);
}
} else {
// No injected wallet — prompt to install one, or offer WalletConnect
}

eth_requestAccounts triggers the connection prompt and must be called from a user gesture such as a click. Calling it on page load is blocked by some wallets and is poor practice regardless.

eth_accounts (no prompt) returns the already-authorised accounts, which is how you restore a connection on reload without asking again.

Always react to the account and chain changing — the user can switch either at any moment:

window.ethereum.on("accountsChanged", (accounts) => { /* re-read state */ });
window.ethereum.on("chainChanged", () => window.location.reload());

The wallet documentation recommends reloading on chainChanged, because contract addresses, balances and cached state are all chain-specific.

With several wallets installed, window.ethereum is whichever one won the race to inject. EIP-6963 solves this by having wallets announce themselves so the user can choose; a connection library implements it for you.

Libraries

LibraryStatusNotes
viemActiveTypeScript-first, small, the current default
ethersActivev6 is a significant change from v5
wagmiActiveReact hooks over viem; handles connection state
web3.jsSunsetChainSafe ended maintenance in 2025

web3.js still works and is what most 2022-era material uses, but it is no longer maintained. New front ends should use viem, or wagmi if the project is React.

npm install viem
npm install ethers
npm install @metamask/detect-provider # provider detection helper
npm install react-toastify # transaction notifications

Rough equivalences when porting:

// web3.js
const balance = await web3.eth.getBalance(address);
web3.utils.fromWei(balance, "ether");
web3.utils.toWei("1", "ether");

// ethers v6
const balance = await provider.getBalance(address);
ethers.formatEther(balance);
ethers.parseEther("1");

ethers v6 returns native BigInt rather than BigNumber, so .add() and .mul() are gone — use + and *. That is the main source of breakage when upgrading from v5.

Transaction feedback

A transaction takes seconds to minutes. The interface has to show that, or users click twice.

const tx = await contract.someMethod(); // wallet prompt; resolves when submitted
toast.info(`Submitted: ${tx.hash}`);

const receipt = await tx.wait(); // resolves when mined
toast.success(`Confirmed in block ${receipt.blockNumber}`);

The promise from a contract call resolves as soon as the transaction is submitted, not when it is mined. Treating that as success is why dapps show a confirmation before anything has happened.

Handle rejection separately from failure — a user declining the wallet prompt is not an error condition:

catch (err) {
if (err.code === 4001) return; // user rejected
toast.error(err.shortMessage ?? err.message);
}

Styling with Tailwind

Utility-first CSS, so the result does not look like an unmodified template. VS Code has first-party support through the Tailwind CSS IntelliSense extension.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Unknown at rule @tailwind

Unknown at rule @tailwindcss(unknownAtRules)

The CSS language server does not recognise Tailwind's directives. Either install the PostCSS Language Support extension, or silence the rule in .vscode/settings.json:

{
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore"
}

It is a warning from the editor only — the build is unaffected either way.

Build issues

webpack 5 no longer polyfills Node core modules

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules
by default.

Web3 libraries reference Node built-ins (crypto, stream, buffer) that webpack 5 stopped shimming automatically. Three options, best first:

  • Use a library that does not need them. viem and ethers v6 are built for browsers and avoid Node built-ins entirely. This removes the problem rather than patching it.
  • Add the polyfills explicitly. With node-polyfill-webpack-plugin, or per module through resolve.fallback.
  • Pin an older toolchain. npm install --save-dev [email protected] pulls in webpack 4. This works and leaves you on an unmaintained build chain with known vulnerabilities — a stopgap, not a fix.

ReferenceError: process is not defined

The same root cause: browser code reaching for a Node global. Define it in the bundler rather than hoping a restart clears it:

// vite.config.js
export default { define: { "process.env": {} } };

Restarting the dev server does sometimes make it go away, but only because the dependency graph is re-optimised — the underlying issue returns.