Skip to main content

Command Palette

Search for a command to run...

Building a Mobile RNS Portfolio Manager

A Production-Ready React Native Guide

Published
29 min read
Building a Mobile RNS Portfolio Manager

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.

  1. iOS: Run cd ios && pod install && cd .. then npm run ios.

  2. 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.

  1. Go to cloud.reown.com (formerly WalletConnect Cloud).

  2. Sign up and click "Create Project".

  3. Name it RnsPortfolioManager.

  4. Select AppKit as the product.

  5. 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.

  1. Start your app: npm run ios or npm run android.

  2. You should see a sleek black screen with a "Connect Wallet" button.

  3. Tap it.

  4. A modal should slide up (The Intercom!).

  5. 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:

  1. The Phonebook (RNS Resolution): Converts 0x... to alice.rsk (one-by-one).

  2. 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.

  1. Quick Route: Use the Mainnet Subgraph (good for testing UI, but uses real data).

  2. 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(),
});

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

  1. Go to The Graph Studio.

  2. Connect your wallet.

  3. Click "Create a Subgraph".

  4. Name: Rns-Testnet-Manager.

  5. 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:

  1. Solved the Deprecation Issue: Instead of relying on broken links, we built our own data infrastructure.

  2. Deployed a Subgraph: You now have a custom API on The Graph Studio that indexes the Testnet Registry.

  3. 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)

Bash

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:

  1. Connected the UI to the API: We wrapped the app in ApolloProvider.

  2. Optimized Rendering: We used FlashList to ensure our scrolling is buttery smooth (60 FPS).

  3. 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:

  1. Drafting: Our app writes the letter ("I, Alice, give this domain to Bob").

  2. Weighing: We ask the post office how much postage (Gas) this letter needs. If we put too little, it gets returned.

  3. 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.

  4. 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:

  1. User taps "Transfer" in RNS Manager.

  2. RNS Manager shows an alert: "Please confirm in MetaMask."

  3. Operating System switches focus to MetaMask.

  4. User sees the gas fee and clicks "Confirm."

  5. MetaMask submits the transaction.

  6. 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:

  1. The Writer: We upgraded our Service to handle Signers, not just Providers.

  2. The Courier: We connected the "Transfer" button to the WalletConnect provider.

  3. 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.

  1. The Icon:

    • Find a nice 1024x1024 png (maybe the RSK logo).

    • Use a generator like appicon.co to generate the Already and ios folders.

    • Replace the files in android/app/src/main/res and ios/RnsManager/Images.xcassets.

  2. The Name:

    • Right now your app is probably called "RnsManager" on the home screen.

    • iOS: Change CFBundleDisplayName in Info.plist.

    • Android: Change app_name in android/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:

  1. A Fortified Setup: You navigated the tricky waters of React Native Polyfills.

  2. A Secure Door: You implemented WalletConnect V2 authentication.

  3. A Smart Brain: You connected to The Graph to index data instantly.

  4. A Beautiful Face: You built a high-performance FlashList UI.

  5. A Strong Hand: You implemented secure blockchain transactions.

Where to go from here?

  • Mainnet: Change chains.ts to ID 30 and rpc.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

Libraries & Tools (The Stack)

Essential Tutorials & Guides

RPC Providers