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

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
.envto 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
| Variable | Purpose |
RSK_MAINNET | Connects to Rootstock Mainnet (ChainID 30) |
RSK_TESTNET | Connects to Rootstock Testnet (ChainID 31) |
WALLET_CONNECT_PROJECT_ID | Enables WalletConnect for dApps |
π Tutorial Checkpoint
Now that environment is set up, let's:
Initialize the wallet
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:
Generate and encrypt mnemonics (with PBKDF2 key derivation).
Build the auth flow (password + biometrics).
Connect to Rootstock (via public RPC nodes).
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-compatworks 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:
Generate it (using battle-tested
ethers.js).Encrypt it (AES-256, like a digital vault).
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!)
Test in dev:
npx ts-node src/utils/wallet/generateWallet.ts(But never commit real mnemonics to Git!)
Backup plan:
Show users their plaintext seed phrase ONCE (during creation).
Warn them: "Write this down. Lose it = lose your funds."
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
Generate β Encrypt β Store

UI Preview:

File:
NewWalletScreen.tsx
π§ What's Next?
Biometric Auth (FaceID/TouchID)
- Already implemented in your
LoginScreen.tsxβ we'll highlight the RSK-specific parts.
- Already implemented in your
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
Balance Check:

(Show RBTC balance)
Network Switch:
const { chain, switchChain } = useNetwork(); switchChain({ chainId: 31 }); // Testnet
π Why This Matters for RSK
Bitcoin-Secured: RSK merges Ethereum's smart contracts with Bitcoin's hash power.
Gas Savings: RSK transactions cost ~5x less than Ethereum.
EVM Compatibility: Your wallet works with any RSK dApp (like Sovryn or MoneyOnChain).
π What's Next?
Choose Your Adventure:
Send RBTC/ERC-20 Tokens
- Implement
SendScreen.tsxwith RSK gas calculations
- Implement
WalletConnect for dApps
- Connect to RSK dApps like Sovryn
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?
ERC-20 Token Transfers
// Coming next: const tx = await tokenContract.transfer(toAddress, amount);Transaction History
- Fetch RSK transactions from the blockchain
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
TransactionHistoryContextAccessible 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 displayLeverages your
ContactsContextperfectly
πΈ UI Integration
(From your existing components)
Home Screen Transactions Preview
// In your Home.tsx const recentTransactions = useTransactionHistory().transactions.slice(0, 3);(Shows last 3 transactions)
Full History View
(Would use your existingTransactionHistoryContextdata)
π§ Pro Tips for Readers
Backup Your History:
// Could add to your database.ts export const backupTransactions = async (txs: Transaction[]) => { await AsyncStorage.setItem('txBackup', JSON.stringify(txs)); };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:
Wallet Backup Flow (Using your existing
seedPhraseencryption)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-svgwith Rootstock logoNetwork Detection: Shows RBTC/tRBTC based on current chainId
Share Options: Native sharing via Expo's
ShareAPI
// 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
ClipboardAPI 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:
QR Code with RSK branding
Masked address (via your
maskAddressutil)Network-appropriate labels
π How This Completes Your Wallet
Now users can:
Send β Your
SendScreen.tsximplementationReceive β This clean receive flow
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:
First Launch:
Checks AsyncStorage for existing wallet
Routes to
Auth(new wallet) orLogin(existing)
Auth Flow:

Session Management:
Auto-syncs chainId across Wagmi, context, and AsyncStorage
Filters tokens by active network
π§ Pro Tips for Readers
Network Persistence:
// Save network preference await AsyncStorage.setItem("chainId", newChainId.toString());Secure Routing:
All routes check
isAuthenticatedFailed auth redirects to
Login
πΈ UI Integration

Header shows current network
Modal lists available networks
Auto-redirect on chain change
β Completed RSKVault Core
You've now documented:
Wallet Creation
RBTC/Token Transfers
Transaction History
Network Management
Navigation Flow
π Final Step Suggestions
Wallet Backup Guide
- Document how to export encrypted mnemonics
Advanced Gas Controls
- Enhance your existing gas utils
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:
β Star the repo to support the project
π Open issues for bugs or feature requests
Submit PRs for improvements
π Where to Go Next?
Deploy to App Stores
- Configure EAS builds for iOS/Android
Extend Functionality
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! π





