Back to experience

Jun 2025–Feb 2026 · Two months of incremental refactoring

MegaWallet

Two months rebuilding cross-platform wallet requests, state, and flows

01

Choose a starting point the team can adopt

With an existing wallet shifting from RN to Electron and changing chain scope, I started with the relatively independent Provider / RPC, tested it separately, and reconnected it gradually.

02

Unify requests while delivering product flows

I brought permission, dedupe, and lock rules into one pipeline, then connected Send and address validation. Seven working documents supported decisions across product, design, and engineering.

03

The project stopped; ownership remained

Main owns real state and chain-kit owns chain capability; platform, permission, and product boundaries became explicit.

RPC
Shared permission, dedupe, and lock pipeline
Host roles
Transport adapters and frontend/backend Service separation
7 docs
Written during delivery and used in team reviews
Send
Send and address validation connected to the new backbone

Chapters

Working documents

Across two months, I wrote and shared seven working documents covering the wallet mental model, architecture risks, DApp requests, Send behavior, address validation, permissions, frontend/backend Service separation, and chain-capability consolidation. They were not retrospective case-study material; they explained context, conflict, trade-offs, and implementation paths while decisions were still being made. Expand one to see why it appeared, what changed next, and its matching chapters.

01Wallet-Core mental-model comparisonSource file · my-history/1.钱包心智模型.mdArchitecture mental modelFilm chapters 03

Core question

Is a wallet an in-memory object that owns data and behavior, or a rendering of database facts at this moment?

Why I wrote it

Establish a shared frame for discussing state, persistence, and cross-host reuse before changing code.

What happened next

The audit separated memory, persistence, and account modeling into explicit lines of inquiry.

02Original architecture risksSource file · my-history/2.suggestions.mdCurrent-state risk auditFilm chapters 03 · 04

Core question

Long-lived plaintext, non-atomic writes, missing migration checks, and an account hierarchy that could not extend naturally.

Why I wrote it

Map an abstract architecture difference onto the MegaWallet code that actually existed.

What happened next

This established that the issue was system-wide and that the first cut should not begin in UI.

04How the wallet handles DApp requestsSource file · my-history/4.Wallet-RPC.mdRequest-system redesignFilm chapters 04 · 05 · 06

Core question

Permission, duplicate prompts, lock behavior, and new-chain support should not remain scattered across Provider, handlers, and Service.

Why I wrote it

Find a relatively independent request boundary that could be replaced first and adopted incrementally.

What happened next

This produced the Provider / Transport and rpc-engine backbone shown in chapters 4–6.

05Send design reviewSource file · my-history/5.Wallet-Send.mdProduct and design reviewFilm chapters 08

Core question

Help product and design see what is still missing between a prototype, a mockup, and real implementation.

Why I wrote it

Once host development recovered, Send became a real delivery task; the static screens did not define dynamic height, input responsibility, or failure states.

What happened next

The review kept contact search while typing and deferred full validation until input completion, leading into document 06.

06useAddressValidationSource file · my-history/6.UseValidation.mdImplementation guideFilm chapters 09

Core question

A small validation toolkit for the Recipient address-input flow.

Why I wrote it

Turn the Send-review interaction rules into composable behavior reusable across pages.

What happened next

This created typing → settled → complete → validating → valid / invalid states and validators.

07Wallet permission mechanismsSource file · my-history/7.SharePermission.mdTeam primerFilm chapters 10

Core question

A product-and-engineering primer covering the standard EOA permission flow and difficult boundary scenarios.

Why I wrote it

Before building DApp connection and confirmation screens, align durable connection with per-action approval.

What happened next

This led to Connector, ApprovalQueue, and one Confirmation UI flow.

08chain-kit and Service architectureSource file · my-history/8.chain-kit-and-service-architecture.mdCommit change summaryFilm chapters 11 · 12

Core question

One change split Service into frontend and backend paths for Electron / Extension; another moved scattered chain capability into chain-kit.

Why I wrote it

One change set touched requests, processes, chains, and keys, so its ownership changes needed to be explicit.

What happened next

Frontend/backend Service separation established the host boundary; chain-kit independently owned chain capability, and redundant or false Services disappeared.

01

I did not inherit a blank wallet; I inherited 217 days of direction debt

Git shows the repository starting on June 5, 2025. By my first core refactor on January 8, 2026, 217 days and 836 commits had accumulated. Expo / React Native arrived in June, then Electron in September; the first desktop build literally copied mobile/dist into the renderer. Solana keyrings, signing, RPC, transactions, and Wallet Standard grew through June–August, while the old Solana Send UI was removed on February 12. Platform and chain scope changed before their boundaries were settled, so earlier assumptions kept stacking.

  • Expo / RN was the first host; Electron later reused the mobile web output
  • Solana moved from explicit investment to an EVM-only product Send path while foundation code remained
  • Requirement changes had no new boundary, so they kept modifying handlers, bridges, and Service
  • Spaghetti code was the consequence of direction debt, not one ugly function

02

Seven documents made the two-month rebuild adoptable by the team

The seven documents covered the Wallet-Core mental model, architecture risks, DApp requests, the Send design review, address validation, and wallet permissions. Document 08 recorded two independent changes: splitting Service into frontend and backend paths for Electron / Extension, and moving chain capability out of crypto, vault, and utils/rpc into chain-kit. They were written for product, design, and engineering before implementation decisions hardened, so each change had shared, reviewable context.

03

Start with Provider / RPC: independent and easier to adopt

Provider and DApp RPC touched every host and chain, but remained relatively independent from UI, assets, and storage. That made them replaceable, testable in isolation, and easier to reconnect incrementally. The old Provider used four inheritance layers to combine EVM state, compatibility methods, events, and platform communication before ending at globalThis.$jsBridge. I replaced it with independent EthereumProvider and SolanaProvider implementations: each owns its state and events, while host communication enters only through Transport. Provider no longer knows the concrete RN, Electron, or Extension API.

04

The same refactor: request policy in rpc-engine

The old path entered service/src/dapp/request.ts, called getHandler(base), and constructed an EVM or Solana Handler for every request. Permission, lock behavior, approval prompts, and RPC fallback remained inside individual methods. New permissions meant editing each method; duplicate prompts required pendingApprovals in handleRequest; a new chain changed Handler classes, maps, types, and base branches. rpc-engine now runs blacklist, logging, error boundaries, permission, CAIP scope, dedupe, lock, and executor in a fixed order, including five protections that did not exist before. Each request ends in a wallet handler, user approval, or RPC proxy.

05

Transport: keep host communication out of Provider

The old architecture split Provider and JS Bridge across two packages. ProviderDelegate depended on globalThis.$jsBridge; the mobile Bridge hard-coded ReactNativeWebView.postMessage, the desktop Bridge hard-coded Electron IPC, and each host needed another buildInject assembly script. A new platform changed both packages, while tests mocked global host objects. Provider now has no host knowledge: React Native, Electron, and Extension each implement a Transport adapter. Supporting another platform means adding one Transport, and tests replace only the interface.

06

After the independent layer was done, the main-project foundation became the blocker

Provider and rpc-engine were complete and reconnected to Service by January 15. Moving them into RN, Electron, and web then exposed the missing host foundation: Electron had no React HMR, WebView DevTools interfered with reload, and cross-host runtime entry points were incomplete. I completed those paths because a refactor is not done when old code disappears; it is done when the new backbone can be developed, debugged, and run inside the real app.

07

UI and foundation work interleaved; this was not a waterfall

After establishing Provider / RPC, I connected product flows incrementally. Send and useAddressValidation landed on January 28. Permission / DApp and the process-aware Service architecture were authored on February 10; chain-kit followed on February 12. Relatively independent modules could be delivered and reconnected early, making each change easier for the team to adopt. UI delivery continued alongside work on permission, state authority, and chain capability.

08

Electron is two programs, but wallet state needs one owner

The project began with React Native, so Service was organized to run directly inside one frontend runtime. Electron later reused the mobile web output and startup model without first defining one main / renderer authority. Renderer is the frontend the user sees; main is the wallet backend that can access local storage, networks, and keys. Their memory is not shared. The old code therefore imported and started Service on both sides, producing two unrelated wallets with the same package name. I moved full Service, RPC Engine, SQLite WAL storage, and signing into main as the only real state, while renderer keeps a sanitized mirror. User actions cross IPC to main; state changes return as Immer patches, with a full snapshot when patch application fails.

09

Consolidate scattered chain logic and false abstractions together

chain-kit was not a directory rename. crypto, vault, and utils/rpc became one function-based chain package; Transfer merged into Tx; the Nonce Service, which never provided its implied concurrency safety, was removed in favor of pending nonce; actions and selectors became write and read boundaries; Zod schemas became runtime validation and type sources. Separately, Signer clears its own Uint8Array buffers after use. Temporary library strings remain an explicit JavaScript limitation; that security enhancement is not evidence of chain-capability consolidation.

010

Durable connections and one-time approvals are different kinds of state

When a site connects, it needs a durable pass recording the site, chain, and visible accounts. Signing, transfers, network changes, and asset requests need only a one-time decision. Connector stores the durable relationship; ApprovalQueue holds the current request and destroys it after approval, rejection, or timeout. Connect, Sign Message, Send Transaction, Switch / Add Network, Watch Asset, and Solana confirmation screens all use this single flow.

011

Send proves the product-definition work

In Figma, the Recipient Picker list state was 378×406 and the complete-address state 378×210. The issue was not merely animation: should the input search contacts or validate an address first? I defined typing, settled, complete, validating, and valid / invalid states, then composed address, domain, self-transfer, contract, address-book, and recent-recipient validators. A fixed three-row list, 64px result area, 300ms debounce, and 600ms settled hint turned continuous jumping into one meaningful transition.

012

A stopped project should not be rewritten as a launch story

This was not a greenfield wallet built in two months. It was incremental replacement inside 217 days and 836 prior commits. The current source contains the independent Provider / Transport, configurable RPC pipeline, a cross-host request backbone reconnected to Service, DApp connection and confirmations, main-authoritative Service, chain-kit, Send, and address validation. The project did not reach market launch, but platform, chain, state, permission, secret, and UI ownership had become explainable.

Commit progression

Jan 8

Replaced the RPC and Provider backbone

Transport adapters, heterogeneous Providers, and the middleware pipeline entered the repository.

Jan 14–15

Finished the backbone and reconnected Service

The independent Provider and rpc-engine entered the main project, exposing its host-infrastructure gaps.

Jan 16–22

Completed the real-host foundation

Cross-host runtime entry points arrived alongside Electron React HMR and the WebView DevTools / reload fix.

Jan 28

Implemented Send and address validation

Design conflicts became input states, validators, caching, and multi-chain rules.

Feb 10

Added connections, approvals, and process authority

Connector, ApprovalQueue, confirmations, and the main/renderer bridge took shape.

Feb 12–13

Closed with chain-kit, blacklist, and cleanup

Chain capability moved from three packages into chain-kit; Transfer and Nonce closed down. Separately, signer buffers gained post-use cleanup.