Building a Mobile RNS Portfolio Manager
A Production-Ready React Native Guide

Technical Level: Intermediate to Advanced
Estimated Build Time: 8–12 Hours
Repository Goal: Production-Ready Mobile dApp on RSK Testnet
Introduction:
Think about the "junk drawer" in your kitchen. You know the one, it’s full of loose batteries, rubber bands, and mysterious keys.
If I told you, "Go get the key to the shed," you’d have to dig through that drawer for ten minutes, trying every silver key until one worked. That is exactly what using a blockchain address (like 0x4a3...9e) feels like for a normal user. It’s messy, confusing, and stressful.
Rootstock Name Service (RNS) is the "Label Maker" for that drawer. Instead of digging for a random silver key, you just grab the one explicitly tagged "Shed Key".
In this tutorial series, we aren’t just building a mobile app; we are building a tool that basically organises that messy drawer for your users. We are going to build a mobile dashboard where a user can type in alice.rsk and instantly see their entire financial portfolio, managed securely on their phone.
We will treat this project like building a custom piece of furniture that’s why we have break down this tutorial in multiple modules(1-6).
Module 1: The Blueprint & Foundation

Goal: Create a mobile environment that speaks "Blockchain" fluent.
Estimated Time: 45 – 60 Minutes
Setting the stage, understanding the architecture, and preparing the environment.
Before we hammer a single nail, we need to make sure our workshop is set up correctly. Mobile blockchain development is slightly trickier than web development because phones (iOS/Android) don't speak the same language as the blockchain (Node.js). We have to teach them how to talk.
1.1 The Ingredients (Prerequisites)
You wouldn’t start baking a cake without checking for milk. Ensure you have these installed on your machine:
Node.js (v18+): The main engine.
Watchman: Helps your computer watch for file changes (essential for React Native).
React Native CLI: We are using the "Real" React Native, not Expo, because we need full control over the native code for crypto libraries.
CocoaPods (Mac only): The dependency manager for iOS.
A Cup of Coffee: Essential fuel. ☕
1.2 Breaking Ground (Project Initialization)
Open your terminal. We are going to create a brand new TypeScript project. I prefer TypeScript because it catches errors before they break your app, like a spellchecker for your code.
Bash
npx @react-native-community/cli init RnsManager
cd RnsManager
Note: This might take a few minutes. It’s downloading the entire internet.
1.3 The "Polyfill" Situation (Crucial Step!)
Here is where 90% of developers quit.
The library we use to talk to the blockchain (ethers.js) expects to be running on a powerful server. It looks for things like crypto (for security) and buffer (for handling raw data). Your iPhone doesn't have these by default. It’s like trying to plug a 3-prong dryer plug into a standard wall outlet.
We need adapters. In code, we call these Polyfills.
Step 1: Install the adapters. Run this command in your terminal:
Bash
npm install --save react-native-get-random-values react-native-url-polyfill buffer events stream-browserify process
react-native-get-random-values: This is the most important one. It generates secure random numbers for your wallet keys. Without this, your app is not secure.buffer: Helps the app read "binary" data (the language of the blockchain).
Step 2: Wire them up. Open your index.js file (this is the very first file your app reads when it wakes up). We need to import these tools at the absolute top, before anything else.
JavaScript
// index.js
// 1. Import the random number generator FIRST.
// This is like putting on your safety goggles before using a saw.
import 'react-native-get-random-values';
// 2. Import the URL polyfill so we can talk to the internet correctly.
import 'react-native-url-polyfill/auto';
// 3. Teach the app what "Buffer" and "Process" are.
import { Buffer } from 'buffer';
global.Buffer = Buffer;
global.process = require('process');
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App);
1.4 Setting the Coordinates (Rootstock Config)
Now that our phone speaks "Blockchain," we need to tell it where to look. We are going to start on the Rootstock Testnet. This is our sandbox where we can play with "fake" money (tRBTC) so we don't accidentally spend real funds while testing.
Create a new folder called src and inside it, a folder called constants. Create a file named chains.ts.
TypeScript
// src/constants/chains.ts
export const ROOTSTOCK_TESTNET = {
id: '31', // This is the unique ID for the RSK Testnet
name: 'Rootstock Testnet',
rpcUrl: 'https://rootstock-testnet.drpc.org', // The phone line we call to get data
currency: {
name: 'Test RBTC',
symbol: 'tRBTC',
decimals: 18,
},
// This is the address of the RNS Registry on Testnet.
// Think of this as the "Phone Book" building address.
rnsRegistryAddress: '0x7d284aaac6e925aad802a53c0c69efe3764597b8',
};
Note: We use https://rootstock-testnet.drpc.org as our RPC (Remote Procedure Call). It’s a reliable public node that connects us to the network.
1.5 The First Test Run
Let’s make sure everything is connected.
iOS: Run
cd ios && pod install && cd ..thennpm run ios.Android: Run
npm run android.
If your app opens a white screen with "Welcome to React Native," congratulations! You have successfully built a mobile environment capable of advanced cryptography.
🏁 Checkpoint: What we just achieved
[x] Built the project skeleton.
[x] Installed the "adapters" (polyfills) so our phone can do crypto math.
[x] Configured the map coordinates for the Rootstock Testnet.
Now, We pick up exactly where we left off, moving from the Setup phase to the Connection phase.
Module 2: The Handshake (Connectivity & Authentication)

Prerequisites: Completion of Module 1.
Estimated Time: 1–2 Hours
Introduction: The "Intercom" Analogy
Think of it as you live in a high-tech apartment complex. You don't have a physical key; you have a digital pass on your phone.
When you walk up to the building (our App), you don't hand your phone to the security guard (the Code) and say, "Here, unlock it yourself." That’s dangerous. Instead, the building has an Intercom. You tap your phone on the intercom, it beeps, verifies you are who you say you are, and then the door opens.
In blockchain, that Intercom is WalletConnect (powered by Reown).
Our app (the building) never touches the user's private key (the phone). We just send a signal to their Wallet (Metamask, Defiant, Trust Wallet), and they decide whether to sign the guest log.
In Module 2, we are installing that Intercom system.
2.1 The Digital Permit (Getting a Project ID)
Before we can use the Intercom network, we need to register our building. This is free and standard for all blockchain apps.
Go to cloud.reown.com (formerly WalletConnect Cloud).
Sign up and click "Create Project".
Name it
RnsPortfolioManager.Select AppKit as the product.
Copy your Project ID.
- Household Note: Treat this ID like your WiFi password. It identifies your app to the network.
Create a Secure Config File: In your project, create a new file .env at the root (where package.json is).
Plaintext
# .env
PROJECT_ID=your_copied_project_id_here
2.2 Installing the Hardware (AppKit SDK)
We need to install the specific tools that let React Native talk to Ethers.js and the WalletConnect network.
Note: As of late 2025, "Web3Modal" has been rebranded to "Reown AppKit". We are using the cutting-edge, production-ready libraries.
Open your terminal and run:
Bash
npm install @reown/appkit-react-native @reown/appkit-ethers-react-native ethers react-native-svg
For iOS Users (The CocoaPods Step): Whenever you add a library with native code, you have to "glue" it into the iOS project.
Bash
cd ios && pod install && cd ..
2.3 Wiring the Intercom (Configuration)
Now we need to create the main configuration file that tells our app: "We are using Rootstock Testnet, and here is our Project ID."
Create a new file: config/AppKitConfig.tsx.
TypeScript
// config/AppKitConfig.tsx
import { createAppKit } from '@reown/appkit-react-native';
import { EthersAdapter } from '@reown/appkit-ethers-react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
// 1. Get the Project ID from our safe storage
// (In a real app, use react-native-dotenv. For now, paste it to test if unsure)
const projectId = 'YOUR_PROJECT_ID_HERE';
const storage: any = {
setItem: async (key: string, value: any) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.warn('Storage setItem error:', e);
}
},
getItem: async <T = any>(key: string): Promise<T | undefined> => {
try {
const value = await AsyncStorage.getItem(key);
return value != null ? JSON.parse(value) : undefined;
} catch (e) {
console.warn('Storage getItem error:', e);
return undefined;
}
},
removeItem: async (key: string) => {
try {
await AsyncStorage.removeItem(key);
} catch (e) {
console.warn('Storage removeItem error:', e);
}
},
};
// 2. Define the Metadata
// This is what shows up on the user's wallet when they connect.
// "RnsManager wants to connect to your wallet"
const metadata = {
name: 'RNS Portfolio Manager',
description: 'Manage your Rootstock domains on the go',
url: 'https://web3spell.com', // Your website
icons: ['https://avatars.githubusercontent.com/u/37784886'],
redirect: {
native: 'rnsmanager://', // Deep link for the app to open back up
},
};
// 3. Define the Chains
// We map our Module 1 constants to the format AppKit expects
const rootstockTestnetChain : any= {
chainId: 31,
name: 'Rootstock Testnet',
currency: 'tRBTC',
explorerUrl: 'https://explorer.testnet.rsk.co',
rpcUrl: 'https://rootstock-testnet.drpc.org',
};
// 4. Initialize the Configuration
export const appKit = createAppKit({
adapters: [new EthersAdapter()],
networks: [rootstockTestnetChain],
projectId,
metadata,
enableAnalytics: true,
storage: storage
});
2.4 The "Connect" Button
Now, let's put the button on the screen. This isn't just a regular button; it's a "Smart Button" provided by the library that handles loading spinners, QR codes, and wallet selection automatically.
Open App.tsx and replace the code with this:
TypeScript
// App.tsx
import React from 'react';
import { SafeAreaView, StyleSheet, Text, View } from 'react-native';
import { AppKitButton } from '@reown/appkit-react-native';
import './config/AppKitConfig'; // Import the config to wake it up!
function App(): JSX.Element {
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>RNS Manager</Text>
<Text style={styles.subtitle}>
Your mobile gateway to Rootstock
</Text>
{/* This is the Magic Button */}
<View style={styles.buttonContainer}>
<AppKitButton
connectButtonLabel="Connect Wallet"
balance="show"
/>
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#111', // Dark mode style
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#fff',
marginBottom: 10,
},
subtitle: {
fontSize: 16,
color: '#aaa',
marginBottom: 40,
textAlign: 'center',
},
buttonContainer: {
marginTop: 20,
},
});
export default App;
2.5 Testing the Handshake
This is the moment of truth.
Start your app:
npm run iosornpm run android.You should see a sleek black screen with a "Connect Wallet" button.
Tap it.
A modal should slide up (The Intercom!).
Since you are on a simulator/emulator, you can't "scan" a QR code easily.
Pro Tip: Choose "WalletConnect" -> and copy the URI if you want to paste it into a real wallet, OR...
Best Way to Test: Run the app on a Physical Device. Shake your phone to open the dev menu, and reload. Tap connect, and select "MetaMask" or "Defiant" if you have them installed. It should jump to the wallet, ask for permission, and jump back.
If the button turns into an address (e.g., 0x4a...9e) and shows a balance, you have successfully bridged the gap between mobile and blockchain.
🚦 Checkpoint: Module 2 Complete
What we just built:
[x] Registered our "Building" (Project ID) with the network.
[x] Installed the "Intercom" (AppKit SDK).
[x] Created the UI to trigger the handshake.
We are now moving from "Connection" to "Communication."
Module 3: The Data Layer (Speaking the Language)

Prerequisites: Completion of Module 2.
Estimated Time: 2 – 3 Hours
Introduction: The "Contacts List" vs. The "Librarian"
Right now, your app shows the user their address: 0x7d2...9bd. That is like greeting your friend by saying, "Hello, 1-555-012-3456!" It’s robotic. You want to say, "Hello, Alice!"
To fix this, we need two tools:
The Phonebook (RNS Resolution): Converts
0x...toalice.rsk(one-by-one).The Librarian (The Graph): Fetches lists of data instantly.
However, there is a catch. The "Public Library" (Hosted Service) for Rootstock Testnet is closed. If we want a librarian, we have to hire one ourselves. In this module, we will set up the RNS Service and then build our own Private Subgraph.
3.1 The Phonebook: Setting up RNS Resolution
Rootstock Name Service (RNS) works exactly like your phone's contacts. We use Ethers.js to look up names.
Step 1: The RNS Service Create services/RnsService.ts. This "Singleton" service is our dedicated translator.
TypeScript
// services/RnsService.ts
import { ethers, Contract } from 'ethers';
import { ROOTSTOCK_TESTNET } from '../constants/chains';
// The "Phone Book" Address (Testnet Registry)
const RNS_REGISTRY_ADDRESS = '0x7d284aaac6e925aad802a53c0c69efe3764597b8';
export class RnsService {
provider: ethers.JsonRpcProvider;
registryContract: ethers.Contract;
constructor() {
this.provider = new ethers.JsonRpcProvider(ROOTSTOCK_TESTNET.rpcUrl);
// Connect to the Registry Contract
this.registryContract = new Contract(
RNS_REGISTRY_ADDRESS,
['function resolver(bytes32 node) view returns (address)'],
this.provider
);
}
// "Who is 0x123...?" (Reverse Resolution)
async lookupAddress(address: string): Promise<string | null> {
try {
return await this.provider.lookupAddress(address);
} catch (error) {
return null;
}
}
}
export const rnsService = new RnsService();
3.2 The Librarian: Setting up The Graph (Advanced Setup)
We need to fetch all domains owned by a user. Doing this via RPC calls is slow. We need The Graph.
The Challenge: The Graph migrated away from their "Hosted Service" in 2023. There is no public, active Subgraph for RNS on Rootstock Testnet right now.
The Solution: You have two options.
Quick Route: Use the Mainnet Subgraph (good for testing UI, but uses real data).
Pro Route (Recommended): Deploy your own Subgraph to The Graph Studio.
Option A: Configuration (Apollo Client)
First, let's install the tools:
Bash
npm install @apollo/client graphql
Now, create src/config/ApolloConfig.ts. We will set it up so you can easily swap between Mainnet (fallback) and your custom Testnet URL.
TypeScript
// config/ApolloConfig.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
// 1. Mainnet Fallback (Public & Decentralized)
// We use this if we just want to test if our UI works with REAL data.
const RNS_SUBGRAPH_URL_MAINNET = 'https://api.thegraph.com/subgraphs/id/DhBgWdhFsujyqFmYqaTwUyyYm5QWBEhqVnBHek9JYPkn';
// 2. Your Custom Testnet URL
// Once you finish the "Workshop" below, paste your Studio URL here.
const RNS_SUBGRAPH_URL_TESTNET = ''; // e.g. https://api.studio.thegraph.com/query/1234/rns-testnet/v0.0.1
// Logic: Use Testnet URL if it exists, otherwise fallback to Mainnet
const RNS_SUBGRAPH_URL = RNS_SUBGRAPH_URL_TESTNET || RNS_SUBGRAPH_URL_MAINNET;
export const apolloClient = new ApolloClient({
link: new HttpLink({ uri: RNS_SUBGRAPH_URL }),
cache: new InMemoryCache(),
});
🛠️ Workshop: Create Your Own Subgraph (Recommended)
Since no public testnet subgraph exists, we are going to build one. This gives you full control and ensures your app works on Testnet.
Prerequisites:
Node.js (v18+) & Yarn
A wallet with Testnet tRBTC (for deploying contracts if needed, though The Graph Studio is free for testnet).
Step 1: Setup The Graph Studio
Go to The Graph Studio.
Connect your wallet.
Click "Create a Subgraph".
Name:
Rns-Testnet-Manager.Select Blockchain: Rootstock Testnet.
Step 2: Initialize the Project Open your terminal. We will use the official boilerplate to save time.
Bash
# 1. Install the CLI
yarn global add @graphprotocol/graph-cli
# 2. Clone the Rootstock Boilerplate
git clone https://github.com/rsksmart/rootstock-subgraph.git
cd rootstock-subgraph
yarn install
# 3. Prepare the Environment
yarn run prepare:RSK:testnet
# (This auto-generates your docker and yaml configs)
Step 3: Define the Schema (schema.graphql) We need to tell the Librarian what a "Domain" looks like. Create or edit schema.graphql:
GraphQL
type Domain @entity {
id: ID! # The Nodehash
name: String! # "alice.rsk"
owner: Bytes! # The Owner's Address
expiration: BigInt # When does it expire?
}
type Transfer @entity {
id: ID!
domain: Domain!
to: Bytes!
blockNumber: BigInt!
}
Step 4: Configure the Manifest (subgraph.yaml) This maps the blockchain events to your schema. Edit subgraph.yaml:
YAML
specVersion: 0.0.4
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: RNSRegistry
network: rootstock-testnet
source:
# The Official Testnet Registry Address (Double Checked!)
address: "0x7d284aaac6e925aad802a53c0c69efe3764597b8"
abi: RNSRegistry
startBlock: 4000000 # Skip old blocks to sync faster
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Domain
abis:
- name: RNSRegistry
file: ./abis/RNSRegistry.json
eventHandlers:
- event: NewOwner(indexed bytes32,indexed bytes32,address)
handler: handleNewOwner
file: ./src/mapping.ts
Note: You will need the RNSRegistry.json ABI file. You can grab it from the RNS Registry Repo.
Step 5: Write the Logic (src/mapping.ts) This is the script that runs every time a domain is bought or sold.
TypeScript
import { NewOwner } from '../generated/RNSRegistry/RNSRegistry';
import { Domain } from '../generated/schema';
export function handleNewOwner(event: NewOwner): void {
let id = event.params.node.toHex();
let domain = Domain.load(id);
if (domain == null) {
domain = new Domain(id);
domain.name = ""; // Name resolution is complex, keeping it simple
}
domain.owner = event.params.owner;
domain.save();
}
Step 6: Deploy! Back in your terminal:
Bash
# Generate code and build
yarn codegen
yarn build
# Authenticate (Get your key from the Studio Dashboard)
graph auth --studio <YOUR_DEPLOY_KEY>
# Deploy
graph deploy --studio rns-testnet-manager
Once deployed, copy the Query URL from your dashboard and paste it into src/config/ApolloConfig.ts.
3.3 Integrating Data into the UI
Now that our "Librarian" (The Graph) is ready, we need to write the specific question we want to ask it. We will save this in a separate file so our UI components can use it easily.
Create queries/DomainQueries.ts:
TypeScript
// queries/DomainQueries.ts
import { gql } from '@apollo/client';
/**
* GraphQL Query to fetch domains owned by a specific wallet.
* @param $ownerId - The wallet address (must be lowercase Bytes)
* @returns List of domains with their name and expiration date.
*/
export const GET_DOMAINS_BY_OWNER = gql`
query GetDomainsByOwner($ownerId: Bytes!) {
domains(where: { owner: $ownerId }) {
id
name
expiration
}
}
`;
Now that our "Private Librarian" is working, let's update App.tsx to use the data.
TypeScript
// App.tsx Updates
import { useEffect, useState } from 'react';
import { useAccount } from '@reown/appkit-react-native';
import { rnsService } from './services/RnsService';
// ... inside your component
const { address, isConnected } = useAccount();
const [rnsName, setRnsName] = useState<string | null>(null);
useEffect(() => {
const fetchName = async () => {
if (address && isConnected) {
// 1. Ask the Phonebook
const name = await rnsService.lookupAddress(address);
setRnsName(name);
}
};
fetchName();
}, [address, isConnected]);
🚦 Checkpoint: Module 3 Complete
What we just achieved:
Solved the Deprecation Issue: Instead of relying on broken links, we built our own data infrastructure.
Deployed a Subgraph: You now have a custom API on The Graph Studio that indexes the Testnet Registry.
Configured Apollo: Your app can now switch between Mainnet and your custom Testnet node.
Module 4: The Portfolio Dashboard (The Display Case)

Prerequisites: Completion of Module 3
Estimated Time: 2 Hours
Introduction: Unpacking the Boxes
Right now, your app is like a warehouse. You have the inventory (data from The Graph), and you have the staff (Apollo Client) to fetch it. But if a customer walks in, they just see a blank wall.
We need to build the Display Case.
In this module, we are going to take that raw data and render it into a beautiful, high-performance list on the user's screen. We will focus heavily on Mobile Performance because scrolling through 100 domains shouldn't make your phone hot enough to fry an egg.
4.1 Wiring the Electricity (The Apollo Provider)
Remember the "Librarian" (Apollo Client) we hired in Module 3? Right now, he is sitting in the break room. We need to put him at the front desk so every screen in our app can ask him questions.
We do this by "wrapping" our entire app in the ApolloProvider.
Open your App.tsx (or index.js if you prefer keeping App clean) and update it:
TypeScript
// App.tsx
import React from 'react';
import { SafeAreaView, StyleSheet } from 'react-native';
import { AppKitButton } from '@reown/appkit-react-native';
import { ApolloProvider } from '@apollo/client/react'; // Import the Provider
import { apolloClient } from './config/ApolloConfig'; // Import our specific client
import './src/config/AppKitConfig';
import DomainList from './components/DomainList'; // We are about to build this!
function App(): JSX.Element {
return (
// Wrap everything inside the Provider.
// Now the "Librarian" can hear requests from anywhere inside these tags.
<ApolloProvider client={apolloClient}>
<SafeAreaView style={styles.container}>
{/* ... Header Text ... */}
<AppKitButton label="Connect Wallet" balance="show" />
{/* The new component we are building today */}
<DomainList />
</SafeAreaView>
</ApolloProvider>
);
}
// ... styles ...
4.2 The "Infinite" Scroll (Shopify FlashList)
Here is a common rookie mistake in React Native: Using the standard FlatList or ScrollView for big lists of data. It’s slow.
We are going to use FlashList by Shopify. It recycles the views (cells) on the screen so it uses hardly any memory, even if your user has 10,000 domains.
Step 1: Install the high-performance list
Bash
npm install @shopify/flash-list
Step 2: iOS Housekeeping (You know the drill, native code needs linking)
cd ios && pod install && cd ..
4.3 Designing the Product Card (The Domain Component)
Before we build the list, let's design the individual card. This is what a single domain looks like on the shelf.
Create components/DomainCard.tsx.
We need to handle a specific piece of math here: Expiration Dates. The blockchain stores dates as "Unix Timestamps" (e.g., 1735689000). Humans hate that. We need to convert it to "Dec 31, 2024".
TypeScript
// components/DomainCard.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
interface DomainCardProps {
name: string;
expiryDate: string; // The Graph returns this as a string
}
const DomainCard = ({ name, expiryDate }: DomainCardProps) => {
// Helper: Convert "1735..." to "12/31/2024"
const formatDate = (timestamp: string) => {
const date = new Date(parseInt(timestamp) * 1000); // Multiply by 1000 for milliseconds
return date.toLocaleDateString();
};
// Check if expired
const isExpired = parseInt(expiryDate) * 1000 < Date.now();
return (
<View style={styles.card}>
<View style={styles.iconPlaceholder}>
<Text style={styles.iconText}>RNS</Text>
</View>
<View style={styles.infoContainer}>
<Text style={styles.domainName}>{name}</Text>
<Text style={[styles.expiryText, isExpired && styles.expiredText]}>
{isExpired ? 'EXPIRED' : `Expires: ${formatDate(expiryDate)}`}
</Text>
</View>
<View style={styles.statusDot} />
</View>
);
};
const styles = StyleSheet.create({
card: {
backgroundColor: '#1E1E1E',
padding: 15,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
// Shadow for depth
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
elevation: 5,
},
iconPlaceholder: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#F7931A', // Rootstock Orange
justifyContent: 'center',
alignItems: 'center',
marginRight: 15,
},
iconText: { fontWeight: 'bold', color: '#fff', fontSize: 10 },
infoContainer: { flex: 1 },
domainName: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginBottom: 4 },
expiryText: { color: '#888', fontSize: 12 },
expiredText: { color: '#FF4444' }, // Red for danger
statusDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#00E676', // Green for active
},
});
export default DomainCard;
4.4 Bringing it Together (The Domain List)
Now, let's create the manager component that asks the Librarian for data and feeds it into our FlashList.
Create components/DomainList.tsx.
TypeScript
// components/DomainList.tsx
import React from 'react';
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
import { useQuery } from '@apollo/client/react';
import { FlashList } from '@shopify/flash-list';
import { GET_DOMAINS_BY_OWNER } from '../queries/DomainQueries'; // From Module 3
import { useAccount } from '@reown/appkit-react-native';
import DomainCard from './DomainCard';
const DomainList = () => {
const { address, isConnected } = useAccount();
// The Magic Hook: This automatically fetches data when 'address' changes.
const { loading, error, data } = useQuery(GET_DOMAINS_BY_OWNER, {
variables: { ownerId: address?.toLowerCase() }, // The Graph stores addresses in lowercase!
skip: !isConnected || !address, // Don't ask if we aren't logged in
});
if (!isConnected) {
return (
<View style={styles.centerContainer}>
<Text style={styles.message}>Please connect your wallet to view domains.</Text>
</View>
);
}
if (loading) return <ActivityIndicator size="large" color="#F7931A" />;
if (error) {
return <Text style={styles.errorText}>Error fetching domains: {error.message}</Text>;
}
// Handle "Empty Drawer" scenario
if (!data || data.domains.length === 0) {
return (
<View style={styles.centerContainer}>
<Text style={styles.message}>No RNS domains found on this address.</Text>
<Text style={styles.subMessage}>Buy one at name.rootstock.io!</Text>
</View>
);
}
return (
<View style={styles.listContainer}>
<FlashList
data={data.domains}
renderItem={({ item }) => (
<DomainCard name={item.name} expiryDate={item.ttl} />
)}
estimatedItemSize={70} // Helps FlashList calculate scroll speed
keyExtractor={(item) => item.id}
/>
</View>
);
};
const styles = StyleSheet.create({
listContainer: { flex: 1, width: '100%', marginTop: 20 },
centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', marginTop: 50 },
message: { color: '#fff', fontSize: 16 },
subMessage: { color: '#888', marginTop: 8 },
errorText: { color: 'red', textAlign: 'center', marginTop: 20 },
});
export default DomainList;
🚦 Checkpoint: Module 4 Complete
What we just built:
Connected the UI to the API: We wrapped the app in
ApolloProvider.Optimized Rendering: We used
FlashListto ensure our scrolling is buttery smooth (60 FPS).Visualized the Data: We built a custom card component that formats blockchain timestamps into human-readable dates.
Module 5: The Action Phase (Transactions & Interaction)

Prerequisites: Completion of Module 4
Estimated Time: 2 – 3 Hours
Introduction: The "Certified Letter" Analogy
Up until now, our app has been a "Read-Only" experience. We were just looking at data. That’s free and easy.
Now, we want to change data (e.g., transfer a domain to a friend). In blockchain terms, this is a State Change.
Think of sending a transaction like sending a Certified Letter:
Drafting: Our app writes the letter ("I, Alice, give this domain to Bob").
Weighing: We ask the post office how much postage (Gas) this letter needs. If we put too little, it gets returned.
Signing: This is the tricky part. Our app does not have the wax seal (Private Key). The User's Wallet App (like MetaMask or Defiant) has the seal.
The Handoff: We have to hand the letter to the Wallet App, ask the user to stamp it with their seal, and then they drop it in the mailbox (Blockchain).
In Module 5, we are building that specific handoff mechanism.
5.1 The "Signer" (The Person with the Stamp)
In Ethers.js, there is a distinct difference between a Provider and a Signer.
Provider: A TV screen. It can show you what's happening, but it can't interact.
Signer: The remote control. It can actually change the channel.
We need to upgrade our connection from a Provider to a Signer.
Update services/RnsService.ts:
We need to add a function that accepts a Signer object to perform actions.
TypeScript
// src/services/RnsService.ts (Add this new method)
import { Contract, BrowserProvider } from 'ethers';
// ... existing code ...
// New Method: Transfer a Domain
// We need 3 things: The domain name, the new owner's address, and the "Signer" (the authorized wallet)
async transferDomain(domainName: string, newOwner: string, provider: BrowserProvider) {
try {
// 1. Get the Signer from the Provider
// This is like asking the user: "Can I borrow your pen?"
const signer = await provider.getSigner();
// 2. Re-connect the contract with the Signer
// The contract was "View Only" before. Now it's "Writeable".
const rnsWithSigner = new Contract(
RNS_REGISTRY_ADDRESS,
['function setOwner(bytes32 node, address owner)'], // The ABI for changing ownership
signer
);
// 3. Calculate the "Namehash" (The ID of the domain)
// RNS doesn't understand "alice.rsk". It understands a massive hash number.
const node = ethers.namehash(domainName);
// 4. Send the Transaction (The "Certified Letter")
console.log(`Transferring ${domainName} to ${newOwner}...`);
const tx = await rnsWithSigner.setOwner(node, newOwner);
// 5. Wait for the Receipt
// We wait for 1 confirmation (1 block) to ensure it's "in the mailbox".
console.log("Transaction sent! Hash:", tx.hash);
await tx.wait(1);
return tx.hash;
} catch (error) {
// This usually happens if the user clicks "Reject" in their wallet
console.error("Transfer failed:", error);
throw error;
}
}
5.2 The User Interface (The Transfer Button)
We need a button on our DomainCard that triggers this action. But wait, mobile interactions are tricky. When you tap "Transfer," the app has to background itself, open MetaMask, wait for the user, and then come back.
Let's modify DomainCard.tsx to include a simple "Transfer" button.
(In a real production app, you'd make a pretty modal popup. For this tutorial, we will use a basic Alert with a text input logic simulated for simplicity).
TypeScript
// components/DomainCard.tsx (Update)
import { useProvider } from '@reown/appkit-react-native';
import { BrowserProvider } from 'ethers';
import { rnsService } from '../services/RnsService';
import { Alert, TouchableOpacity } from 'react-native';
// ... inside the component ...
const { walletProvider } = useProvider('eip155'); // Get the raw connection
const handleTransfer = async () => {
if (!walletProvider) return;
// In a real app, use a Modal to get this address.
// For this tutorial, we hardcode a friend's address or asking user via prompt logic.
const newOwner = "0x...FriendAddress...";
try {
// 1. Wrap the wallet provider in Ethers so we can talk to it
const ethersProvider = new BrowserProvider(walletProvider);
Alert.alert("Check your Wallet", "Please switch to your wallet app to sign the transaction.");
// 2. Call our service
const txHash = await rnsService.transferDomain(name, newOwner, ethersProvider);
Alert.alert("Success!", `Domain transferred.\nTx Hash: ${txHash}`);
} catch (error: any) {
if (error.code === 'ACTION_REJECTED') {
Alert.alert("Cancelled", "You rejected the transaction.");
} else {
Alert.alert("Error", "Something went wrong. Do you have enough tRBTC for gas?");
}
}
};
return (
<View style={styles.card}>
{/* ... existing UI ... */}
<TouchableOpacity style={styles.transferBtn} onPress={handleTransfer}>
<Text style={styles.btnText}>Transfer</Text>
</TouchableOpacity>
</View>
);
// Add styles...
// transferBtn: { backgroundColor: '#333', padding: 8, borderRadius: 6, marginTop: 10 }
5.3 The Gas Station (Avoiding "Out of Gas")
This is a hidden trap in blockchain.
If you send a letter with only one stamp, but the post office says it weighs 2 ounces, they throw it away and keep your stamp. This is an "Out of Gas" error. You lose money and the transaction fails.
To prevent this, we usually estimate gas before sending.
Ethers.js usually handles this automatically, but on mobile networks (which can be flaky), it's good to know how to do it manually if needed:
TypeScript
// Optional: Manual Gas Estimation (For advanced stability)
const gasEstimate = await rnsWithSigner.setOwner.estimateGas(node, newOwner);
// Add a 20% "safety buffer" just in case the network gets busy while we are sending
const safeGasLimit = (gasEstimate * 120n) / 100n;
const tx = await rnsWithSigner.setOwner(node, newOwner, { gasLimit: safeGasLimit });
5.4 Handling the "App Switching" Dance
On mobile, the user experience flow looks like this:
User taps "Transfer" in RNS Manager.
RNS Manager shows an alert: "Please confirm in MetaMask."
Operating System switches focus to MetaMask.
User sees the gas fee and clicks "Confirm."
MetaMask submits the transaction.
User manually switches back to RNS Manager.
The "Stuck" Spinner Problem: Sometimes, users forget to come back. Your app sits there spinning forever. The Fix: Always have a timeout or a "Close" button on your loading spinners.
📚 Checkpoint: Module 5 Complete
What we just built:
The Writer: We upgraded our Service to handle
Signers, not justProviders.The Courier: We connected the "Transfer" button to the WalletConnect provider.
The Safety Net: We added error handling for when users say "No" (User Rejected).
The Final Frontier (Module 6) Now, We have got a working app. We can view domains (Read) and transfer them (Write). But... it looks like a developer tool. In the final module, we will polish it. We will handle Network Switching (what if the user is on Mainnet but needs Testnet?), cleanup the UI, and prepare the final build settings for the App Store.
Module 6: The Final Polish & Launch

Prerequisites: Completion of Module 5.
Estimated Time: 1 Hour
Introduction: Tuning the Radio
Imagine you build a beautiful radio station that plays smooth jazz (Rootstock). But when your users turn on their radio (Wallet), it’s stuck on the heavy metal station (Ethereum Mainnet).
They try to interact with your app, but the radio just hisses static. Why? Because they are on the wrong frequency.
In this final module, we are going to build the Auto-Tuner. We will ensure that no matter what network the user starts on, our app gently guides them to Rootstock. We will also add the "fresh coat of paint" that makes an app feel professional, like "Pull to Refresh."
6.1 The Network Enforcer (Auto-Switching)
Mobile wallets are notorious for staying on Ethereum Mainnet by default. If a user tries to send a Rootstock transaction while on Ethereum, it will fail instantly.
We need to listen to the network state and prompt a switch if they drift away.
Update App.tsx (or your main wrapper):
We will use a hook from the AppKit to watch the chainId.
TypeScript
// components/NetworkGuard.tsx
import React, { useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useAppKit, useAccount } from '@reown/appkit-react-native';
import { ROOTSTOCK_TESTNET } from '../constants/chains';
export const NetworkGuard = ({ children }: { children: React.ReactNode }) => {
const { switchNetwork } = useAppKit();
const { isConnected, chainId } = useAccount();
// Rootstock Testnet ID is 31. (Mainnet is 30).
const TARGET_CHAIN_ID = 31;
if (isConnected && chainId !== TARGET_CHAIN_ID) {
return (
<View style={styles.guardContainer}>
<Text style={styles.warningText}>Wrong Network Detected</Text>
<Text style={styles.subText}>
You are currently on Chain ID {chainId}.
Please switch to Rootstock Testnet to manage your domains.
</Text>
<TouchableOpacity
style={styles.switchBtn}
onPress={() => switchNetwork({
id: TARGET_CHAIN.id,
name: TARGET_CHAIN.name,
nativeCurrency: TARGET_CHAIN.currency,
rpcUrls: {
default: { http: ['https://public-node.testnet.rsk.co']
}
},
chainNamespace: 'eip155',
caipNetworkId: `${TARGET_CHAIN.id}:${TARGET_CHAIN.id}`
})}
>
<Text style={styles.btnText}>Switch to Rootstock</Text>
</TouchableOpacity>
</View>
);
}
// If everything is fine, render the app normally
return <>{children}</>;
};
const styles = StyleSheet.create({
guardContainer: {
flex: 1,
backgroundColor: '#000',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
warningText: { color: '#FF4444', fontSize: 22, fontWeight: 'bold', marginBottom: 10 },
subText: { color: '#fff', textAlign: 'center', marginBottom: 20 },
switchBtn: { backgroundColor: '#F7931A', padding: 15, borderRadius: 8 },
btnText: { color: '#fff', fontWeight: 'bold' },
});
How to use it: Wrap your DomainList inside this Guard in App.tsx. Now, if a user wanders off to Ethereum, the app locks the screen until they come back.
6.2 The "Did it Work?" Feature (Pull-to-Refresh)
In Blockchain, things aren't instant. A user buys a domain, and it might take 30 seconds to show up. Users have a natural instinct to "pull down" the screen to check for updates.
We need to satisfy that instinct.
Update src/components/DomainList.tsx:
We will add the refreshControl prop to our FlashList.
TypeScript
import { RefreshControl } from 'react-native';
// ... inside your component ...
const { loading, error, data, refetch } = useQuery(GET_DOMAINS_BY_OWNER, {
// ... existing options ...
notifyOnNetworkStatusChange: true, // Important for showing loading state on refetch
});
const [refreshing, setRefreshing] = React.useState(false);
const onRefresh = React.useCallback(() => {
setRefreshing(true);
// Tell Apollo to ignore the cache and ask the server again
refetch().then(() => setRefreshing(false));
}, []);
return (
<View style={styles.listContainer}>
<FlashList
data={data.domains}
// ... existing props ...
// The Pull-to-Refresh Magic
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#F7931A" // Make the spinner Orange (iOS)
colors={['#F7931A']} // Make the spinner Orange (Android)
/>
}
/>
</View>
);
6.3 Final Housekeeping (Logos & Assets)
Before you show this to the world, you need to replace the default React Native robot icon.
The Icon:
Find a nice 1024x1024 png (maybe the RSK logo).
Use a generator like
appicon.coto generate theAlreadyandiosfolders.Replace the files in
android/app/src/main/resandios/RnsManager/Images.xcassets.
The Name:
Right now your app is probably called "RnsManager" on the home screen.
iOS: Change
CFBundleDisplayNameinInfo.plist.Android: Change
app_nameinandroid/app/src/main/res/values/strings.xml.
6.4 The Grand Conclusion
Congratulations! You have just graduated from the Mobile RNS Masterclass.
Let's look at what you built:
A Fortified Setup: You navigated the tricky waters of React Native Polyfills.
A Secure Door: You implemented WalletConnect V2 authentication.
A Smart Brain: You connected to The Graph to index data instantly.
A Beautiful Face: You built a high-performance FlashList UI.
A Strong Hand: You implemented secure blockchain transactions.
Where to go from here?
Mainnet: Change
chains.tsto ID30andrpc.rootstock.io.Auctions: Add a feature to bid on RNS domains directly from the app.
Push Notifications: Use Reown's "Web3Inbox" to alert users when their domain is expiring.
Your Homework: Take this code, push it to GitHub, and tweet a screenshot of your working app tagging @Rootstock_Io. You are now a mobile Web3 developer.
References and Resources:
Code Repo: https://github.com/rajeevK07/Mobile-RNS-Portfolio-Manager
Official Rootstock Documentation
Rootstock Developer Portal: dev.rootstock.io
- The bible for everything Rootstock. Start here.
Rootstock Name Service (RNS) Docs: dev.rootstock.io/developers/integrate/rns/
- Detailed breakdown of the Registry, Resolvers, and Architecture.
RNS SDK Reference: https://github.com/rsksmart/rns-sdk/
- If you want to read the whole SDK docs and code we chipped in.
Rootstock Testnet Faucet: faucet.rootstock.io
- Get free tRBTC to test your app without spending real money.
Rootstock Explorer (Testnet): explorer.testnet.rootstock.io
- View your transactions and contract interactions in real-time.
Libraries & Tools (The Stack)
Reown (formerly WalletConnect) AppKit: docs.reown.com/appkit/react-native
- The official guide for the mobile wallet connection SDK.
The Graph on Rootstock: thegraph.com/docs/en/supported-networks/rootstock/
- Official guide on how The Graph indexes Rootstock data.
Ethers.js (v6) Documentation: docs.ethers.org/v6
- Documentation for the library we used to "talk" to the blockchain.
Shopify FlashList: shopify.github.io/flash-list
- Performance tuning tips for the infinite scroll list we built.
Essential Tutorials & Guides
Rootstock "Zero to Hero": dev.rootstock.io/guides/quickstart/
- A broader overview of deploying smart contracts on RSK.
React Native Crypto Docs: npmjs.com/package/react-native-crypto
- A deep dive into why
bufferandstreamare needed on mobile (The "Junk Drawer" problem).
- A deep dive into why
Graph Subgraphs : thegraph.com/docs/en/subgraphs/quick-start/
- where you can practice writing GraphQL queries before putting them in your code.
RPC Providers
Rootstock Public Nodes: dev.rootstock.io/node-operators/public-nodes/
- List of available public RPC endpoints if you want to switch away from the default.
