Rainforest Cheng
Back to portfolio

Case study

Hoogii Wallet

Frontend Engineer · Hashgreen Labs · 2022 – Present

A browser-extension wallet for the Chia blockchain — send, receive and manage XCH and CATs, and review transaction history.

Hoogii Wallet landing page — a fully open-source Chia crypto wallet extension
1 / 4

Landing page

Typing a wallet into existence

A 12-cell backup-phrase grid that validates each word as you type and accepts a whole pasted phrase.

I built the custom phrase input, and made the call to open create and import in a full browser tab instead of the 360-pixel popup — key material deserves room to breathe.

The grid is a react-hook-form useFieldArray. A cell turns red only when it is non-empty and missing from the BIP39 wordlist_en.json set — a membership test, never a network call. Paste splits on spaces and distributes across the remaining cells.

Recovery phrase · 12 words
0/12

Mirrors src/components/Mnemonic.tsx · src/store/MnemonicStore.ts

Finding a token before you finish typing it

A typo-tolerant fuzzy filter over the CAT list, tuned strict so it behaves like asset selection, not web search.

I owned the search surface and the tuning call: threshold 0.1 on purpose, plus a multi-token $and query so "usd coin" narrows instead of widening.

Search is fuse.js configured in utils/fuse.ts with threshold: 0.1 and includeScore. The query is rebuilt as an $and of tokens, each an OR across the keys, then results sort by score. It runs over an already-live array — never a fetch per keystroke.

  • XC
    XCH
    Chia
  • SB
    SBX
    Spacebucks
  • DB
    DBX
    dexie bucks
  • MR
    MRMT
    Marmot
  • WO
    WOLF
    Chia Wolf
  • US
    USDS
    Stably USD
  • TI
    TIBET
    TibetSwap
  • WU
    WUSDC
    Wrapped USDC
  • HO
    HOA
    Hoge Ants
  • CA
    CATC
    Cool Cats
  • GO
    GOAT
    Chia Goat
  • PE
    PENG
    Penguin

Mirrors src/utils/fuse.ts · src/components/SearchBar.tsx

The ledger that updates itself

History and balances that update themselves — an incoming coin appears on its own, no polling.

This was the realtime layer I built — a channel per wallet, refetch on message, and enough defensiveness that a flaky socket degrades quietly instead of taking the screen down.

The channel name is literally '0x' + puzzleHash, so each wallet listens only on its own hash. Ably.tsx is a try/catch around useChannel that returns an empty fragment — a dropped socket logs and the UI stays up.

12.5 XCH
channel 0x1a2b…9f3e
live
  • Received
    from xch1c8d…44fa
    +2.0 XCH
  • Sent
    to xch1a91…09be
    -0.75 XCH
  • Received
    from xch1f30…7c2d
    +5.0 XCH

Mirrors src/components/Ably.tsx

What locking actually does

Encrypt the phrase at rest, require the password to unlock, auto-lock when the machine goes idle.

I owned the lock lifecycle: the seed is never decrypted at rest, the password lives only in ephemeral session memory, and auto-lock leans on the browser's own idle signal rather than a timer I'd have to babysit.

encryption.ts derives an AES-GCM key with PBKDF2 · SHA-512 · 100,000 iterations · 32-byte salt. Auto-lock hooks chrome.idle.setDetectionInterval, not setInterval — real input resets the countdown.

Wallet session · idle auto-lockactive
idle · auto-locks when the bar fills0.0s / 5.0s

Mirrors src/utils/encryption.ts

When a website asks the wallet to sign

A website asks the wallet to sign; the request crosses three script contexts and comes back a signature.

This is the core of what I owned — built with one other engineer: inject the provider, relay every request across page, content and background, gate by lock and connection state, then show a signature a human can actually read.

A request crosses three contexts. The injected script exposes window.chia.hoogii; the background enforces a fixed gate order — IS_VALID_WALLETIS_LOCKIS_CONNECTED — before forwarding. signCoinSpends raises the popup, which renders the spend bundle as an expandable tree via react-json-view-lite.

  1. IS_VALID_WALLET
  2. IS_LOCK
  3. IS_CONNECTED
blocked at IS_CONNECTED
  • idle — trigger a signing request to trace it.

Mirrors src/injected-scripts/index.ts · src/background/index.ts · src/popup/