Wallet

The wallet extension

Entry #21 · 2026-04-02 · Devlog

The wallet extension

MetaMask couldn't do what we needed. Here's why, and what we built instead.

the metamask gap

When you click "+ Add to MetaMask" on the explorer, MetaMask adds AsentumChain as a network — chain ID 1337, ASE as the native token, the RPC URL filled in. What it does NOT do is give you a Dilithium3 address. MetaMask generates a secp256k1 address from your seed, because that's all MetaMask knows how to do. Our chain doesn't accept secp256k1 transactions — eth_sendRawTransaction intentionally returns "method not found."

We built a MetaMask Snap back in Phase 4.2 that holds a Dilithium3 keypair inside MetaMask's sandbox. It works, it's tested, but even if we published it to NPM, the Snap wouldn't appear in MetaMask's main accounts list alongside ETH and BTC. It's a "request-handling" Snap, not a "Keyring" Snap, and even Keyring Snaps require users to dig through Settings → Snaps → install. Not the UX we want.

So we built a native Chrome extension.

what the extension does

Asentum Wallet — Manifest V3, zero-framework, 5 bundled scripts totaling ~800 KB. One popup, one background service worker, one confirm window, one content script bridge, one in-page provider. Available as a sideload zip at testnet.asentum.com/install/asentum-extension.zip, Chrome Web Store submission pending review.

The popup is a mini SPA with 10 views: onboarding welcome, password setup, lock screen, account view, send, receive, history, manage accounts, add account, settings. All rendered with plain JS string templates and event listeners, matching the explorer's design tokens so it looks like one product.

Key features:

  • Password-encrypted keys. scrypt (N=131072, r=8, p=1) + AES-GCM-256. Private keys never touch disk as plaintext. The decrypted keypair lives in chrome.storage.session — wiped on browser restart or explicit lock.
  • Multiple accounts. Create, rename, switch, delete. Each new account re-prompts for the wallet password (verified against the active account before generating the new keypair).
  • Transaction history. Scans the last 200 blocks (~6 minutes of chain time) and shows every transaction involving the active account.
  • Per-origin permissions. First-time origins get a connect-confirmation popup. Granted origins persist and are revocable from Settings → Connected Sites.
  • Per-transaction approval. Every sendTransfer, deployContract, and callContract from a dApp opens a separate confirm window showing the origin, action, amount/method, and estimated gas fee. No silent signing.

the provider API

The extension injects window.asentum into every web page via a MAIN-world content script. Any dApp can call:

const { address } = await window.asentum.connect();
const { txHash } = await window.asentum.sendTransfer({ to, amount });
const { txHash } = await window.asentum.deployContract({ source });
const { txHash } = await window.asentum.callContract({ to, method, args });
const { result } = await window.asentum.viewContract({ to, method, args });
await window.asentum.disconnect();

The message flow: page → window.postMessage → content script (isolated world) → chrome.runtime.sendMessage → background service worker → sign with the session-cached Dilithium3 keypair → broadcast via the RPC → return tx hash. Five contexts, one async chain.

the <all_urls> bug

First deploy: the extension loaded, the popup worked, but window.asentum was never injected into pages. No errors anywhere. Took an hour of debugging to find: Chrome 145 separates content_scripts match patterns (where scripts want to run) from host_permissions (where they're allowed to run). Our manifest had broad content_scripts matches but only three specific host_permissions URLs. Chrome silently restricted injection to just those three sites — and even those were toggled off by default for sideloaded extensions.

Fix: "host_permissions": ["<all_urls>"]. Same thing MetaMask, Phantom, and every other wallet extension declares. Triggers an in-depth Chrome Web Store review, but that's the standard for wallets.

the numbers

The extension reuses ~500 lines from existing packages (wallet-core.ts from the Phase 4.2 Snap, rpc-client.ts from the CLI) and adds ~2,000 lines of new code across 8 source files. Five bundles: popup.js (430 KB), background.js (395 KB), confirm.js (8 KB), content-script.js (1.4 KB), provider.js (2.8 KB). Total installed size: ~850 KB. Cold open time: instant (under 100ms to render the popup).

— milkie

Don't miss the next entry.

Join the launch list and we'll send you a note whenever there's a new devlog entry, a research drop, or a real milestone.