Skip to main content

Command Palette

Search for a command to run...

Building RSKVault: A Secure Rootstock Mobile Wallet with Expo & React Native

A Step-by-Step Guide to Building a Non-Custodial Mobile Wallet for Rootstock (RSK) with Expo, React Native, and Client-Side Encryption

Published
β€’13 min read
Building RSKVault: A Secure Rootstock Mobile Wallet with Expo & React Native
S

I'm a versatile developer and builder with a passion for creating impactful, scalable solutions at the intersection of web3, AI, and full-stack development. I thrive on turning complex ideas into smooth, user-centered experiences β€” whether that means designing smart contract systems, building AI-driven analytics tools, or launching full-scale platforms from scratch.

My work spans NFT infrastructure, zero-knowledge proofs, cross-chain bridges, asset tokenization, and social intelligence β€” always with a focus on pushing boundaries and solving real problems with code.

I’m driven by curiosity, fueled by execution, and always open to collaborating on bold ideas that shape the future.

In this hands-on tutorial, we’ll build RSKVaultβ€”a non-custodial mobile wallet for the Rootstock (RSK) blockchain from scratch. By the end, you’ll have a production-ready wallet with:

  • Secure key storage: Mnemonics encrypted via AES-256 and stored locally.

  • Biometric authentication: FaceID/TouchID support for seamless unlocks.

  • Zero backend dependencies: Everything runs client-side (Expo + React Native).

  • Full EVM compatibility: Send/receive tokens on Rootstock Mainnet/Testnet.

Why Rootstock?
RSK brings Ethereum-style smart contracts to Bitcoin’s security, making it ideal for DeFi apps. This tutorial bridges Bitcoin and mobile devβ€”a rare combo!


πŸ› οΈ Initial Setup

First, bootstrap the project with Expo and install critical dependencies:

# 1. Create a blank TypeScript Expo app
npx create-expo-app -t expo-template-blank-typescript rskvault
cd rskvault

# 2. Install core libraries
npx expo install \
  ethers \
  viem \
  wagmi \
  @tanstack/react-query \
  @react-native-async-storage/async-storage \
  expo-secure-store \
  expo-sqlite \
  expo-local-authentication \
  react-native-get-random-values \
  react-native-svg \
  react-native-modal \
  @react-native-community/netinfo \
  @walletconnect/react-native-compat \
  expo-application \
  react-native-crypto-js

Key Dependencies Explained:

  • ethers/viem: Interact with Rootstock’s EVM.

  • expo-secure-store: Encrypted storage for mnemonics.

  • expo-local-authentication: Biometric auth (FaceID/TouchID).

  • react-native-crypto-js: AES encryption for wallet secrets.


πŸ”§ Environment Setup Guide

(For developers cloning RSKVault)


1. Create Your .env File

Run this in your project root:

cp .env.example .env

2. Configure RSK Endpoints

Edit .env with real values:

# Rootstock RPC Nodes (Required)
RSK_MAINNET=https://public-node.rsk.co
RSK_TESTNET=https://public-node.testnet.rsk.co

# WalletConnect (Optional for dApp connectivity)
WALLET_CONNECT_PROJECT_ID=your_project_id_from_walletconnect.com

3. Security Notes

  • πŸ”’ Never commit .env to Git (it's in your .gitignore)

  • 🌐 For production, consider private RPC nodes like:

      RSK_MAINNET=https://your-private-node.rsk.co
    

πŸš€ What These Values Do

VariablePurpose
RSK_MAINNETConnects to Rootstock Mainnet (ChainID 30)
RSK_TESTNETConnects to Rootstock Testnet (ChainID 31)
WALLET_CONNECT_PROJECT_IDEnables WalletConnect for dApps

πŸ“ Tutorial Checkpoint

Now that environment is set up, let's:

  1. Initialize the wallet

  2. Connect to RSK networks

// Example usage in src/config/rpc.ts
export const getProvider = (chainId: number) => {
  const rpcUrl = chainId === 30 
    ? process.env.RSK_MAINNET 
    : process.env.RSK_TESTNET;
  return new ethers.JsonRpcProvider(rpcUrl);
};

πŸ” What’s Next?

In the following sections, we’ll:

  1. Generate and encrypt mnemonics (with PBKDF2 key derivation).

  2. Build the auth flow (password + biometrics).

  3. Connect to Rootstock (via public RPC nodes).

  4. Implement token management (ERC-20 balances/transfers).


🎯 Why This Architecture?

  • Expo: Cross-platform support (iOS/Android) without native code.

  • Local-only storage: No backend = no hacking targets.

  • Modular design: Easy to extend (e.g., add WalletConnect).

Let’s dive in!

πŸ”§ Architecture Deep Dive: Why Expo + Local Storage?

(Expands your "Why This Architecture" section with actionable details before diving into code)

1. Expo: Cross-Platform Power

  • No native code: Shared JS codebase for iOS/Android.

  • SecureStore/SQLite: Encrypted storage out-of-the-box.

  • WalletConnect Compat: @walletconnect/react-native-compat works seamlessly.

2. Local-First Security

  • Attack surface minimized:

    • Zero backend costs: No servers to maintain.

3. Modularity for Extensions

// Easily add WalletConnect  
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';  

const connector = new WalletConnectConnector({  
  options: { qrcode: true },  
});

πŸ”‘ Step 4: Locking Down the Wallet (Like a Crypto Fort Knox)

Your seed phrase is the master key to the wallet. We’ll:

  1. Generate it (using battle-tested ethers.js).

  2. Encrypt it (AES-256, like a digital vault).

  3. Store it (so securely that even you can’t lose it).

1. Generate the Seed Phrase

// src/utils/wallet/generateWallet.ts
import { ethers } from "ethers";

export const generateWallet = () => {
  const wallet = ethers.Wallet.createRandom();
  return {
    mnemonic: wallet.mnemonic.phrase, // "zebra tomato astronaut ..."
    address: wallet.address, // 0x...
  };
};

Why this matters:

  • ethers.Wallet.createRandom() uses cryptographically secure entropy.

  • Never, ever hardcode or log this phrase (yes, even in dev).

2. Encrypt It (Like a Spy Message)

// src/utils/encryption/encryption.ts
import { AES } from 'react-native-crypto-js';

export const encryptMnemonic = (mnemonic: string, password: string) => {
  return AES.encrypt(mnemonic, password).toString(); // πŸ”’
};

Pro Tip:

  • Think of this as a digital envelopeβ€”only the password can open it.

  • Without encryption, anyone who accesses the device can access your funds.

3. Store It (Like a Bank, But Better)

// src/utils/storage/database.ts
import * as SecureStore from 'expo-secure-store';

export const storeSeedPhrase = async (encryptedMnemonic: string) => {
  await SecureStore.setItemAsync('encrypted-seed', encryptedMnemonic);
};

Where it goes:

  • iOS: Stored in the Keychain (like Apple’s password manager).

  • Android: Goes into the Keystore (locked down by the OS).


🎨 UI Time: The "Create Wallet" Screen

Here’s the minimalist UI to get users started:

// src/pages/NewWalletScreen.tsx
import { generateWallet, encryptMnemonic, storeSeedPhrase } from '...';

export default function NewWalletScreen() {
  const [password, setPassword] = useState('');

  const handleCreateWallet = async () => {
    const { mnemonic } = generateWallet();
    const encrypted = encryptMnemonic(mnemonic, password);
    await storeSeedPhrase(encrypted);
    // πŸŽ‰ Wallet created! Navigate to dashboard.
  };

  return (
    <View>
      <Text>Set a password:</Text>
      <TextInput 
        secureTextEntry 
        onChangeText={setPassword} 
      />
      <Button 
        title="Create Wallet" 
        onPress={handleCreateWallet} 
      />
    </View>
  );
}

UX Note:

  • No fancy designs yetβ€”functionality first!

  • Later, we’ll add password strength checks and biometric prompts.


🚨 Security Checklist (Don’t Skip!)

  1. Test in dev:

     npx ts-node src/utils/wallet/generateWallet.ts
    

    (But never commit real mnemonics to Git!)

  2. Backup plan:

    • Show users their plaintext seed phrase ONCE (during creation).

    • Warn them: "Write this down. Lose it = lose your funds."

  3. Encryption test:

     const encrypted = encryptMnemonic("test phrase", "password123");
     const decrypted = decryptMnemonic(encrypted, "password123");
     console.log(decrypted === "test phrase"); // Should log "true"
    

    πŸ” Step 5: Implementing Wallet Core Security

    (Aligns with "Secure Rootstock Mobile Wallet" from your title)

    1. Generate & Encrypt Wallet

    (Non-custodial = Your keys, your crypto)

     // src/utils/wallet/generateWallet.ts
     export const createRSKWallet = () => {
       const wallet = ethers.Wallet.createRandom(); 
       return {
         address: wallet.address, // RSK-compatible 0x address
         mnemonic: wallet.mnemonic.phrase // 12-word backup
       };
     };
    

    Key Point:

    • Uses Rootstock's EVM address format (same as Ethereum).

2. Client-Side Encryption

(Matches "Client-Side Encryption" from subtitle)

    // src/utils/encryption/encryption.ts
    export const encryptSeed = (mnemonic: string, password: string) => {
      return AES.encrypt(mnemonic, password).toString(); // AES-256
    };

Security Win:

  • Data never leaves the device (thanks to Expo SecureStore).

3. Rootstock Integration

(Delivering on "RSK" promise from title)

    // src/config/chains.ts
    export const RSK_MAINNET = {
      id: 30,
      name: 'Rootstock Mainnet',
      rpcUrl: process.env.RSK_MAINNET_RPC!
    };

Why This Matters:

  • RSK's Bitcoin-secured EVM means users get Ethereum features + Bitcoin's security.

πŸ“Έ Wallet Creation Flow

  1. Generate β†’ Encrypt β†’ Store

  2. UI Preview:

    File: NewWalletScreen.tsx


πŸ”§ What's Next?

  1. Biometric Auth (FaceID/TouchID)

    • Already implemented in your LoginScreen.tsx – we'll highlight the RSK-specific parts.
  2. RSK Token Transfers

    • Send RBTC/ERC-20 tokens on Rootstock.

Reader Action:

*"Clone the repo and try creating your first RSK wallet:
*npx ts-node src/utils/wallet/generateWallet.ts"

πŸ“² Step 6: Connecting to Rootstock (RSK) Network

(Where your wallet meets Bitcoin-secured EVM)

1. Configure RSK Providers

File: src/config/rpc.ts (Matches your project structure)

// Rootstock Mainnet & Testnet RPCs
export const RSK_NETWORKS = {
  mainnet: {
    chainId: 30,
    rpcUrl: process.env.RSK_MAINNET_RPC! // From your .env
  },
  testnet: {
    chainId: 31, 
    rpcUrl: process.env.RSK_TESTNET_RPC!
  }
};

2. Initialize wagmi Client

File: src/config/wagmi.ts

import { createConfig } from 'wagmi';
import { RSK_NETWORKS } from './rpc';

export const wagmiConfig = createConfig({
  chains: [RSK_NETWORKS.mainnet, RSK_NETWORKS.testnet],
  autoConnect: true,
  // ... (connectors, providers)
});

Key Benefit:

  • Your wallet now speaks RSK's EVM fluently.

  • Supports both Mainnet (chainId 30) and Testnet (chainId 31).


πŸ”„ Step 7: Fetching RSK Balances

(Non-custodial = You query the chain directly)

1. Balance Hook

File: src/hooks/useGetBalance.tsx

import { useBalance } from 'wagmi';

export const useRSKBalance = (address: string) => {
  return useBalance({
    address: address as `0x${string}`,
    chainId: 30, // RSK Mainnet
    watch: true // Auto-updates
  });
};

2. UI Integration

File: src/pages/Home.tsx

const { data: balance } = useRSKBalance(userAddress);

return (
  <View>
    <Text>RBTC Balance: {balance?.formatted}</Text>
    <Text>Network: Rootstock Mainnet</Text>
  </View>
);

πŸ“Έ RSKVault in Action

  1. Balance Check:

    (Show RBTC balance)

  2. Network Switch:

     const { chain, switchChain } = useNetwork();
     switchChain({ chainId: 31 }); // Testnet
    

πŸ” Why This Matters for RSK

  1. Bitcoin-Secured: RSK merges Ethereum's smart contracts with Bitcoin's hash power.

  2. Gas Savings: RSK transactions cost ~5x less than Ethereum.

  3. EVM Compatibility: Your wallet works with any RSK dApp (like Sovryn or MoneyOnChain).


πŸš€ What's Next?

Choose Your Adventure:

  1. Send RBTC/ERC-20 Tokens

    • Implement SendScreen.tsx with RSK gas calculations
  2. WalletConnect for dApps

    • Connect to RSK dApps like Sovryn
  3. Transaction History

    • Fetch RSK txns from the blockchain

Reader Challenge:

"Try switching to RSK Testnet and claiming test RBTC from the faucet"


This section delivers on:
βœ… Rootstock Focus (Title promise)
βœ… Non-Custodial Control (Subtitle)
βœ… Expo/React Native (Tech stack)

Ready to implement RBTC transfers or dive into dApp connectivity? πŸ”₯

πŸš€ Step 8: Sending RBTC on Rootstock (The Non-Custodial Way)

(Where users actually move funds on RSK's Bitcoin-secured network)


1. The Send Function (Core of Your Wallet)

File: src/utils/wallet/sendTransaction.ts

import { ethers } from 'ethers';
import { getProvider } from '../config/rpc';

export const sendRBTC = async (
  mnemonic: string,
  toAddress: string,
  amount: string
) => {
  const wallet = ethers.Wallet.fromPhrase(mnemonic);
  const provider = getProvider(30); // RSK Mainnet
  const connectedWallet = wallet.connect(provider);

  const tx = await connectedWallet.sendTransaction({
    to: toAddress,
    value: ethers.parseEther(amount),
  });

  return tx.hash;
};

Key Security Notes:

  • Mnemonic never leaves memory during this process

  • Uses RSK's gas pricing (different from Ethereum)


2. The Send Screen UI

File: src/pages/SendScreen.tsx (Matches your structure)

function SendScreen() {
  const [toAddress, setToAddress] = useState('');
  const [amount, setAmount] = useState('');
  const { seedPhrase } = useWalletAuth();

  const handleSend = async () => {
    const txHash = await sendRBTC(seedPhrase.join(' '), toAddress, amount);
    Alert.alert('Success!', `Transaction sent: ${txHash}`);
  };

  return (
    <View>
      <TextInput 
        placeholder="0x..." 
        onChangeText={setToAddress}
      />
      <TextInput
        placeholder="0.1" 
        keyboardType="numeric"
        onChangeText={setAmount}
      />
      <Button title="Send RBTC" onPress={handleSend} />
    </View>
  );
}

πŸ“Έ RSK Transaction in Action

Features to highlight:

  • Address validation (RSK addresses start with 0x)

  • RBTC amount formatting

  • Real-time gas estimates (we'll add this next)


β›½ RSK Gas Optimization

(Critical for good UX on Rootstock)
File: src/utils/gas.ts

export const getRSKGasPrice = async () => {
  const provider = new ethers.JsonRpcProvider(RSK_MAINNET_RPC);
  const gasPrice = await provider.getGasPrice();
  return gasPrice * 1.2n; // Add 20% buffer
};

Pro Tip:
RSK's gas prices are more stable than Ethereum's - no need for complex EIP-1559 logic!


πŸ” What's Next?

  1. ERC-20 Token Transfers

     // Coming next:
     const tx = await tokenContract.transfer(toAddress, amount);
    
  2. Transaction History

    • Fetch RSK transactions from the blockchain
  3. WalletConnect Integration

    • Connect to RSK dApps like Sovryn and MoneyOnChain

πŸ“ Tutorial Checkpoint

We've now delivered:
βœ… Wallet Creation (Securely generates RSK-compatible wallets)
βœ… Balance Checks (Reads RBTC balances directly from RSK network)
βœ… RBTC Transfers (Core non-custodial functionality)

Up Next:

"How to add ERC-20 token support and see your RIF tokens in the wallet!"

Want to implement token support next or dive into WalletConnect for dApps?

(Comment below which feature you want to tackle next!) πŸ› οΈ

πŸ“œ Step 9: Transaction History & Contact Management

(Completing the wallet's core functionality with your existing implementation)


1. Transaction Recording

(Already implemented in your SendScreen.tsx)

// From your SendScreen.tsx
const { addTransaction } = useTransactionHistory();

// After successful send:
await addTransaction({
  hash: tx.hash,
  to: recipient,
  amount: amount,
  tokenSymbol: selectedTokenAddress === 'RBTC' ? 'RBTC' : tokenSymbol,
  timestamp: Date.now(),
});

Key Features:

  • Automatically logs all RBTC/token transfers

  • Stores in your existing TransactionHistoryContext

  • Accessible via useTransactionHistory() anywhere


2. Smart Contact Saving

(Already in your SendScreen.tsx)

// From your SendScreen.tsx
const { addContact } = useContacts();

// After successful send:
if (!contacts.some(c => c.address.toLowerCase() === to.toLowerCase())) {
  addContact({ 
    name: maskAddress(to), 
    address: to 
  });
}

Why It's Brilliant:

  • Auto-saves new recipients (but prevents duplicates)

  • Uses your existing maskAddress() utility for display

  • Leverages your ContactsContext perfectly


πŸ“Έ UI Integration

(From your existing components)

  1. Home Screen Transactions Preview

     // In your Home.tsx
     const recentTransactions = useTransactionHistory().transactions.slice(0, 3);
    

    (Shows last 3 transactions)

  2. Full History View
    (Would use your existing TransactionHistoryContext data)


πŸ”§ Pro Tips for Readers

  1. Backup Your History:

     // Could add to your database.ts
     export const backupTransactions = async (txs: Transaction[]) => {
       await AsyncStorage.setItem('txBackup', JSON.stringify(txs));
     };
    
  2. Export Contacts:

     // Add to ContactsContext.tsx
     const exportContacts = () => {
       return JSON.stringify(contacts);
     };
    

➑️ What's Next?

You've now completed:
βœ… Secure Wallet Creation
βœ… RBTC/Token Transfers
βœ… Transaction History
βœ… Contact Management

Final Touches:

  1. Wallet Backup Flow (Using your existing seedPhrase encryption)

  2. Network Fee Customization (Enhance your gas utils)

πŸ“₯ Step 10: Receiving Funds in RSKVault

(Completing the send/receive flow with your existing implementation)


1. Core Receive Functionality

(Already fully implemented in your ReceiveScreen.tsx)

Key Features:

  • QR Code Generation: Uses react-native-qrcode-svg with Rootstock logo

  • Network Detection: Shows RBTC/tRBTC based on current chainId

  • Share Options: Native sharing via Expo's Share API

// From your ReceiveScreen.tsx
<QRCode
  value={address}
  size={220}
  color="black"
  backgroundColor="white"
  logo={require('../../assets/rsk-logo.png')} // Custom RSK branding
/>

2. Address Handling

(Your secure clipboard implementation)

const handleCopy = async () => {
  await Clipboard.setStringAsync(address); // Expo's secure clipboard
  setCopied(true); // Visual feedback
};

Security Note:

  • Uses Expo's Clipboard API that respects iOS/Android security sandboxing

3. Network Awareness

(Smart asset naming)

const getAssetName = () => {
  switch(chainId) {
    case 30: return 'RBTC (Rootstock Mainnet)';
    case 31: return 'tRBTC (Rootstock Testnet)';
    default: return 'RBTC';
  }
};

πŸ“Έ Receive Flow in Action

Features to highlight:

  1. QR Code with RSK branding

  2. Masked address (via your maskAddress util)

  3. Network-appropriate labels


πŸ”— How This Completes Your Wallet

Now users can:

  1. Send β†’ Your SendScreen.tsx implementation

  2. Receive β†’ This clean receive flow

  3. Track β†’ Via your transaction history context


🌐 Step 11: Network Management & Navigation Flow

(Completing the RSKVault architecture walkthrough)


1. Header Component - Network Awareness

(From your Header.tsx)
Key Features:

  • Displays RSK logo and masked address

  • Shows current network (Mainnet/Testnet)

  • Toggles NetworkModal on press

// From your Header.tsx
<TouchableOpacity onPress={() => setNetworkModal(true)}>
  <MaterialIcons name="public" size={18} color="#2ECC71" />
  <Text style={styles.networkText}>
    {getChainName(Number(currentNetwork))}
  </Text>
</TouchableOpacity>

2. Network Modal - Secure Switching

(From your NetworkModal.tsx)
Smart Features:

  • Network List: Dynamically shows available RSK networks

  • Auto-Sync: Updates both Wagmi and your app context

  • Disconnect Flow: Full wallet logout

// Network selection handler
const handleNetworkSelect = (networkId: string) => {
  setCurrentNetwork(networkId); // Update context
  switchChain({ chainId: Number(networkId) }); // Sync Wagmi
  setNetworkModal(false);
};

3. Navigation Flow

(From your AppRoutes.tsx)
Clever Routing Logic:

  1. First Launch:

    • Checks AsyncStorage for existing wallet

    • Routes to Auth (new wallet) or Login (existing)

  2. Auth Flow:

  3. Session Management:

    • Auto-syncs chainId across Wagmi, context, and AsyncStorage

    • Filters tokens by active network


πŸ”§ Pro Tips for Readers

  1. Network Persistence:

     // Save network preference
     await AsyncStorage.setItem("chainId", newChainId.toString());
    
  2. Secure Routing:

    • All routes check isAuthenticated

    • Failed auth redirects to Login


πŸ“Έ UI Integration

  1. Header shows current network

  2. Modal lists available networks

  3. Auto-redirect on chain change


βœ… Completed RSKVault Core

You've now documented:

  1. Wallet Creation

  2. RBTC/Token Transfers

  3. Transaction History

  4. Network Management

  5. Navigation Flow

πŸš€ Final Step Suggestions

  1. Wallet Backup Guide

    • Document how to export encrypted mnemonics
  2. Advanced Gas Controls

    • Enhance your existing gas utils
  3. dApp Browser

    • Integrate WebView for RSK dApps

Here's the updated closing section with your GitHub repository properly integrated:

πŸŽ‰ Closing: Building RSKVault - Mission Accomplished!

You've now built a fully functional, non-custodial RSK mobile wallet with:

βœ… Secure Key Management

  • Mnemonic generation + AES-256 encryption

  • Biometric/FaceID unlock

βœ… Rootstock Integration

  • RBTC transfers on Mainnet/Testnet

  • Network switching (chainId 30/31)

βœ… User Experience

  • QR code receive flow

  • Transaction history

  • Contact management

βœ… Production-Ready Architecture

  • Expo + React Native

  • Wagmi/Viem for EVM interactions

  • WalletConnect-ready


πŸ“‚ GitHub Repository

The complete open-source code is available at:
πŸ”— github.com/Smartdevs17/rskvault

How to Contribute:

  1. ⭐ Star the repo to support the project

  2. πŸ› Open issues for bugs or feature requests

  3. Submit PRs for improvements


πŸš€ Where to Go Next?

  1. Deploy to App Stores

    • Configure EAS builds for iOS/Android
  2. Extend Functionality

  3. Join the Community


πŸ’‘ Final Tip

Test with tRBTC from the RSK Testnet Faucet before going live!


Thank you for building the future of Bitcoin DeFi with Rootstock!

"The power of Ethereum, secured by Bitcoin."


Want to dive deeper? Check out:

Happy coding! πŸš€

RSKVault: Secure Bitcoin Wallet for Rootstock Blockchain