Rivellum

Rivellum Portal

Checking...
testnet

Mobile Integration Guide

Guide for integrating Rivellum into mobile apps using React Native, WebViews, and the TypeScript SDK.

Overview

Rivellum provides mobile-friendly APIs for:

  • React Native apps - Full blockchain integration via TypeScript SDK
  • Mobile WebViews - Embed Rivellum features in native apps
  • Unity mobile games - See Unity Integration Guide
  • External wallet flows - Deep link integration with natos-wallet

React Native Integration

Installation

npm install @rivellum/sdk
# or
yarn add @rivellum/sdk

Basic Setup

import { MobileRpcClient, MobileSigner } from '@rivellum/sdk/mobile';

// Create client
const client = new MobileRpcClient({
  nodeUrl: 'https://mainnet.rivellum.io',
  timeout: 15000
});

// Get balance
const balance = await client.getBalance('0x...');
console.log(`Balance: ${balance.rivl} RIVL`);

Mobile Signer Implementation

import { MobileSigner } from '@rivellum/sdk/mobile';

// Dev-only: Local key storage
class LocalMobileSigner implements MobileSigner {
  constructor(
    public address: string,
    private privateKey: Uint8Array
  ) {}

  async signIntent(intentBytes: Uint8Array): Promise<Uint8Array> {
    // Use react-native-crypto or similar for Ed25519 signing
    const signature = await signEd25519(this.privateKey, intentBytes);
    return signature;
  }
}

// Production: External wallet via deep links
class ExternalWalletSigner implements MobileSigner {
  constructor(public address: string) {}

  async signIntent(intentBytes: Uint8Array): Promise<Uint8Array> {
    // Open natos-wallet app via deep link
    const encoded = base64Encode(intentBytes);
    const deepLink = `rivellum://sign?intent=${encoded}&callback=myapp://signed`;
    
    await Linking.openURL(deepLink);
    
    // Wait for callback with signature
    return new Promise((resolve, reject) => {
      const handleUrl = ({ url }: { url: string }) => {
        if (url.startsWith('myapp://signed')) {
          const params = parseUrlParams(url);
          resolve(base64Decode(params.signature));
          Linking.removeEventListener('url', handleUrl);
        }
      };
      
      Linking.addEventListener('url', handleUrl);
      
      // Timeout after 60s
      setTimeout(() => {
        reject(new Error('Signing timeout'));
        Linking.removeEventListener('url', handleUrl);
      }, 60000);
    });
  }
}

Send Transaction

import { IntentBuilder } from '@rivellum/sdk';

async function sendRivl(to: string, amount: number) {
  // Get current nonce
  const balance = await client.getBalance(signer.address);
  
  // Build intent
  const intent = new IntentBuilder()
    .forSender(signer.address)
    .withNonce(balance.nonce)
    .transfer(to, amount)
    .build();
  
  // Simulate first
  const simulation = await client.simulateIntent(intent);
  if (!simulation.success) {
    throw new Error(`Simulation failed: ${simulation.errorMessage}`);
  }
  
  // Send
  const result = await client.sendIntent(intent, signer);
  console.log(`Sent! Intent ID: ${result.intentId}`);
}

Event Subscription

// Subscribe to events for user's address
const unsubscribe = client.subscribeEvents(
  {
    addresses: [userAddress],
    eventTypes: ['transfer', 'nft_minted']
  },
  (event) => {
    console.log(`Event: ${event.type}`, event.data);
    
    // Update UI
    if (event.type === 'transfer') {
      refreshBalance();
    }
  }
);

// Cleanup
useEffect(() => {
  return () => unsubscribe();
}, []);

WebView Integration

HTML Setup

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Rivellum Wallet</title>
</head>
<body>
  <div id="app"></div>
  
  <script src="https://cdn.jsdelivr.net/npm/@rivellum/sdk@latest/dist/bundle.js"></script>
  <script>
    const { MobileRpcClient } = RivellumSDK;
    
    const client = new MobileRpcClient({
      nodeUrl: 'https://mainnet.rivellum.io'
    });
    
    async function loadBalance(address) {
      const balance = await client.getBalance(address);
      document.getElementById('balance').textContent = 
        `${balance.rivl} RIVL`;
    }
  </script>
</body>
</html>

Native App Integration

iOS (Swift)

import WebKit

class RivellumWebView: UIViewController, WKScriptMessageHandler {
    var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let config = WKWebViewConfiguration()
        config.userContentController.add(self, name: "rivellumBridge")
        
        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.loadHTMLString(htmlContent, baseURL: nil)
        
        view.addSubview(webView)
    }
    
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        if message.name == "rivellumBridge" {
            guard let body = message.body as? [String: Any],
                  let action = body["action"] as? String else { return }
            
            switch action {
            case "signIntent":
                // Handle signing request
                let intentBytes = body["intentBytes"] as! String
                signWithNativeWallet(intentBytes)
            default:
                break
            }
        }
    }
}

Android (Kotlin)

class RivellumWebView : AppCompatActivity() {
    private lateinit var webView: WebView
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        webView = WebView(this)
        webView.settings.javaScriptEnabled = true
        
        // Add JavaScript interface
        webView.addJavascriptInterface(
            RivellumBridge(this),
            "RivellumBridge"
        )
        
        webView.loadDataWithBaseURL(
            null,
            htmlContent,
            "text/html",
            "UTF-8",
            null
        )
        
        setContentView(webView)
    }
    
    class RivellumBridge(private val context: Context) {
        @JavascriptInterface
        fun signIntent(intentBytes: String): String {
            // Handle signing with native wallet
            return signWithNativeWallet(intentBytes)
        }
    }
}

Mobile-Friendly SDK APIs

MobileRpcClient Interface

export interface MobileRpcClient {
  /**
   * Get account balance and state
   */
  getBalance(address: string): Promise<BalanceResponse>;
  
  /**
   * Simulate an intent before sending
   */
  simulateIntent(intent: Intent): Promise<SimulationResult>;
  
  /**
   * Send a signed intent to the network
   */
  sendIntent(
    intent: Intent,
    signer: MobileSigner
  ): Promise<SendIntentResult>;
  
  /**
   * Subscribe to blockchain events
   * Returns unsubscribe function
   */
  subscribeEvents(
    filter: EventFilter,
    onEvent: (event: RivellumEvent) => void
  ): () => void;
}

MobileSigner Interface

export interface MobileSigner {
  /**
   * Account address this signer represents
   */
  address: string;
  
  /**
   * Sign an intent
   */
  signIntent(intentBytes: Uint8Array): Promise<Uint8Array>;
}

Response Types

interface BalanceResponse {
  address: string;
  rivl: number;
  assets: Record<string, number>;
  nonce: number;
}

interface SimulationResult {
  success: boolean;
  estimatedFee: number;
  errorMessage?: string;
  stateDiff?: Record<string, any>;
}

interface SendIntentResult {
  intentId: string;
  status: string;
  message?: string;
}

interface RivellumEvent {
  type: string;
  height: number;
  timestamp: number;
  data: Record<string, any>;
  intentId?: string;
}

Deep Link Wallet Integration

URL Scheme Registration

iOS Info.plist

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
    <key>CFBundleURLName</key>
    <string>com.example.myapp</string>
  </dict>
</array>

<key>LSApplicationQueriesSchemes</key>
<array>
  <string>rivellum</string>
  <string>natos-wallet</string>
</array>

Android AndroidManifest.xml

<activity android:name=".MainActivity">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="myapp" />
  </intent-filter>
</activity>

Signing Flow

async function requestSignature(intent: Intent): Promise<Uint8Array> {
  const intentBytes = encodeIntent(intent);
  const intentBase64 = base64Encode(intentBytes);
  
  // Construct deep link
  const deepLink = `rivellum://sign?intent=${intentBase64}&callback=myapp://signed`;
  
  // Open natos-wallet
  await Linking.openURL(deepLink);
  
  // Wait for callback
  return new Promise((resolve, reject) => {
    const handler = ({ url }: { url: string }) => {
      if (url.startsWith('myapp://signed')) {
        const params = new URLSearchParams(url.split('?')[1]);
        const signature = base64Decode(params.get('signature')!);
        
        Linking.removeEventListener('url', handler);
        resolve(signature);
      }
    };
    
    Linking.addEventListener('url', handler);
    
    // Timeout
    setTimeout(() => {
      Linking.removeEventListener('url', handler);
      reject(new Error('Signing timeout'));
    }, 60000);
  });
}

Best Practices

1. Handle Network Conditions

async function robustRpcCall<T>(
  operation: () => Promise<T>,
  retries = 3
): Promise<T> {
  for (let i = 0; i < retries; i++) {
    try {
      return await operation();
    } catch (error) {
      if (i === retries - 1) throw error;
      
      // Exponential backoff
      await new Promise(resolve => 
        setTimeout(resolve, Math.pow(2, i) * 1000)
      );
    }
  }
  throw new Error('Unreachable');
}

// Usage
const balance = await robustRpcCall(() => 
  client.getBalance(address)
);

2. Cache Aggressively on Mobile

class CachedRpcClient {
  private cache = new Map<string, { data: any; timestamp: number }>();
  private cacheTtl = 5000; // 5 seconds
  
  async getBalance(address: string): Promise<BalanceResponse> {
    const cached = this.cache.get(`balance:${address}`);
    if (cached && Date.now() - cached.timestamp < this.cacheTtl) {
      return cached.data;
    }
    
    const balance = await this.client.getBalance(address);
    this.cache.set(`balance:${address}`, {
      data: balance,
      timestamp: Date.now()
    });
    
    return balance;
  }
}

3. Optimize for Battery Life

// Use event subscriptions sparingly
let subscription: (() => void) | null = null;

// Subscribe only when app is active
AppState.addEventListener('change', (state) => {
  if (state === 'active') {
    subscription = client.subscribeEvents(filter, onEvent);
  } else {
    subscription?.();
    subscription = null;
  }
});

4. Secure Key Storage

// React Native
import * as SecureStore from 'expo-secure-store';

async function savePrivateKey(key: Uint8Array) {
  // ONLY for dev/testing - use external wallet in production
  const base64Key = base64Encode(key);
  await SecureStore.setItemAsync('rivellum_dev_key', base64Key);
}

async function loadPrivateKey(): Promise<Uint8Array | null> {
  const base64Key = await SecureStore.getItemAsync('rivellum_dev_key');
  return base64Key ? base64Decode(base64Key) : null;
}

5. Progressive Enhancement

// Check if external wallet is installed
async function isNatosWalletInstalled(): Promise<boolean> {
  try {
    const canOpen = await Linking.canOpenURL('natos-wallet://');
    return canOpen;
  } catch {
    return false;
  }
}

// Fallback to web wallet if native app not installed
async function getSigner(address: string): Promise<MobileSigner> {
  const hasNatosWallet = await isNatosWalletInstalled();
  
  if (hasNatosWallet) {
    return new ExternalWalletSigner(address);
  } else {
    // Open web wallet in browser
    await Linking.openURL(`https://wallet.rivellum.io/connect?callback=myapp://`);
    return await waitForWebWalletConnection();
  }
}

Next Steps


Questions?