DEV Community

Cover image for Polyfills, Shims, and Native Modules: Lessons from Building a React Native Crypto Wallet
Adil Mezghouti
Adil Mezghouti

Posted on

Polyfills, Shims, and Native Modules: Lessons from Building a React Native Crypto Wallet

Preface: This article is about my experience building a crypto mobile wallet back in 2021.

Intro

Building a crypto wallet in React Native is a masterclass in edge-case architecture. Unlike standard mobile applications that simply fetch JSON from a REST API, a crypto wallet acts as an isolated cryptographic engine. It must generate entropy, derive keys, sign transactions, encode raw binary payloads, and communicate with decentralized networks.

When you attempt to bring standard Web3 JavaScript libraries into React Native, you quickly discover a fundamental disconnect: most Web3 packages were written assuming either a full Node.js runtime or a web browser environment. React Native's JavaScript engine (Hermes or JavaScriptCore) provides neither.

To make a mobile crypto wallet work, you inevitably have to master three distinct strategies: Polyfilling, Shimming, and Native Modules. Here is how each strategy fits into building a wallet, when to reach for them, and how the ecosystem has evolved.


The Three Strategies in a Wallet Architecture

                       ┌──────────────────────────────────────────┐
                       │       React Native Wallet Application    │
                       └────────────────────┬─────────────────────┘
                                            │
        ┌───────────────────────────────────┼───────────────────────────────────┐
        ▼                                   ▼                                   ▼
┌───────────────────────┐       ┌───────────────────────┐       ┌───────────────────────┐
│       POLYFILLS       │       │         SHIMS         │       │    NATIVE MODULES     │
├───────────────────────┤       ├───────────────────────┤       ├───────────────────────┤
│ • crypto.getRandom    │       │ • global.Buffer       │       │ • iOS Secure Enclave  │
│   Values              │       │ • readable-stream     │       │ • Android Keystore    │
│ • TextEncoder/Decoder │       │ • path / events       │       │ • C++ secp256k1 via   │
│ • URL/URLSearchParams │       │   (via Metro aliases) │       │   JSI for fast signing│
└───────────────────────┘       └───────────────────────┘       └───────────────────────┘

Enter fullscreen mode Exit fullscreen mode

Polyfilling: Restoring Missing Web Standards

  • What it is: Code that detects if a standard Web API exists in the JavaScript runtime. If missing, it dynamically attaches a spec-compliant JS implementation directly to the global scope.
  • Wallet Context: Web3 SDKs (like Ethers.js, Viem, or WalletConnect) rely heavily on browser primitives for RPC formatting, address validation, and entropy generation. Without polyfills, functions expecting URL or TextEncoder crash immediately.
// Polyfilling entropy generation for bip39 seed phrase generation
import 'react-native-get-random-values';

// Now standard Web Crypto entropy works globally in JS
const randomBytes = crypto.getRandomValues(new Uint8Array(32));

Enter fullscreen mode Exit fullscreen mode

Shimming: Adapting Node.js Ecosystem Dependencies

  • What it is: Intercepting API calls to wrap, adapt, or alias non-standard environments (specifically Node.js built-in libraries) so they run inside React Native.
  • Wallet Context: Core crypto primitives like bip39, ethereumjs-util, bs58, and hdkey were originally written for Node.js. They rely on built-ins like Buffer, stream, and crypto. Metro doesn't resolve these by default, requiring shims and bundler aliases to map them to React Native JS equivalents.

1. Global Injection (In your entry point, e.g., index.js)

Before any Web3 or crypto library imports execute, you inject required Node globals into globalThis:

// In index.js / entry point
global.Buffer = require('react-native-buffer').Buffer;

Enter fullscreen mode Exit fullscreen mode

2. Metro Bundler Aliasing (metro.config.js)

Injecting globals isn't enough on its own—when a package internally executes require('crypto') or require('stream'), Metro will still fail to resolve the module. You must configure Metro to alias Node's built-in modules to React Native-compatible browser/JS polyfill packages:

// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

const defaultConfig = getDefaultConfig(__dirname);

const config = {
  resolver: {
    // Map Node.js core modules to RN-compatible npm packages
    extraNodeModules: {
      crypto: require.resolve('react-native-quick-crypto'),
      stream: require.resolve('readable-stream'),
      buffer: require.resolve('react-native-buffer'),
    },
  },
};
...
Enter fullscreen mode Exit fullscreen mode

Native Modules (TurboModules / JSI): Hardware Security & High-Perf C++

  • What it is: Writing native code (Swift/Objective-C on iOS, Kotlin/Java on Android, or C++ via JSI) to expose native capabilities, security hardware, or compiled C/C++ libraries to JavaScript.
  • Wallet Context: A pure JavaScript engine running heavy cryptographic routines can choke the single-threaded event loop, leading to very poor performance.

Top comments (0)