Rivellum

Rivellum Portal

Checking...
testnet

Unity & Game Integration Guide

Complete guide for integrating Rivellum blockchain into Unity games and interactive applications.

Overview

The Rivellum Unity SDK (Rivellum.Unity) provides game developers with:

  • āœ… Cross-platform support - Works on Windows, macOS, iOS, Android
  • āœ… Simple async/await APIs - Modern C# patterns for Unity 6.x+
  • āœ… Intent simulation - Test transactions before sending
  • āœ… Event streaming - React to blockchain events in real-time
  • āœ… Flexible signing - Dev keys for testing, external wallet for production
  • āœ… NFT support - Mint, transfer, and query NFTs directly from games
  • āœ… Type-safe builders - Fluent API for constructing intents

Installation

Option 1: Source Integration (Recommended)

  1. Copy the rivellum-unity-sdk/src/ folder into your Unity project's Assets/Plugins/ directory:
YourUnityProject/
ā”œā”€ā”€ Assets/
│   └── Plugins/
│       └── Rivellum.Unity/
│           ā”œā”€ā”€ Models/
│           ā”œā”€ā”€ Signing/
│           ā”œā”€ā”€ Transport/
│           ā”œā”€ā”€ RivellumClient.cs
│           ā”œā”€ā”€ IntentBuilder.cs
│           └── ...
  1. Ensure your Unity project targets .NET Standard 2.1 or .NET 4.x:

    • Edit → Project Settings → Player → Other Settings
    • Set Api Compatibility Level to .NET Standard 2.1
  2. Install Newtonsoft.Json via Unity Package Manager:

    • Window → Package Manager
    • Click + → Add package from git URL
    • Enter: com.unity.nuget.newtonsoft-json

Option 2: DLL Import

  1. Build the SDK as a DLL:
cd rivellum-unity-sdk
dotnet build -c Release
  1. Copy bin/Release/netstandard2.1/Rivellum.Unity.dll to your Unity project's Assets/Plugins/ folder.

Quick Start

1. Create a RivellumClient

using Rivellum.Unity;
using Rivellum.Unity.Models;
using System.Threading.Tasks;
using UnityEngine;

public class RivellumManager : MonoBehaviour
{
    private RivellumClient _client;

    async void Start()
    {
        // Configure client
        var config = new RivellumClientConfig
        {
            NodeUrl = "https://devnet.rivellum.io", // Or your local node
            TimeoutMs = 15000,
            AutoSimulateBeforeSend = true
        };

        _client = new RivellumClient(config);

        // Fetch balance
        var balance = await _client.GetBalanceAsync("0x...");
        Debug.Log($"Balance: {balance.GetRivlBalance()} RIVL");
    }

    void OnDestroy()
    {
        _client?.Dispose();
    }
}

2. Send a Simple Transfer

using Rivellum.Unity.Signing;

async Task SendTransfer()
{
    // Create dev signer (testing only!)
    var signer = await LocalDevSigner.CreateNewAsync("password");
    var senderAddress = signer.GetAddress();

    // Get current nonce
    var balance = await _client.GetBalanceAsync(senderAddress);

    // Build intent
    var intent = new IntentBuilder()
        .ForSender(senderAddress)
        .WithNonce(balance.Nonce)
        .Transfer("0x_recipient_address_", 1000000) // 1 RIVL
        .Build();

    // Send (auto-simulates first if configured)
    var result = await _client.SendIntentAsync(intent, signer);
    Debug.Log($"Intent sent! ID: {result.IntentId}");
}

Client Configuration

RivellumClientConfig Properties

var config = new RivellumClientConfig
{
    // Node RPC endpoint
    NodeUrl = "http://localhost:8080",
    
    // Request timeout (ms)
    TimeoutMs = 10000,
    
    // Retry attempts for network errors
    MaxRetries = 3,
    
    // Initial retry delay (ms), doubles each retry
    RetryBackoffMs = 500,
    
    // Auto-simulate before sending
    AutoSimulateBeforeSend = true,
    
    // Only send if simulation predicts success
    RequireSuccessPrediction = true
};

Custom Logger

public class UnityLogger : IRivellumLogger
{
    public void Info(string message) => Debug.Log($"[Rivellum] {message}");
    public void Warn(string message) => Debug.LogWarning($"[Rivellum] {message}");
    public void Error(string message, Exception ex = null) 
        => Debug.LogError($"[Rivellum] {message}\n{ex}");
}

var client = new RivellumClient(config, logger: new UnityLogger());

Wallet & Signing

IIntentSigner Interface

All signing operations use the IIntentSigner interface:

public interface IIntentSigner
{
    Task<byte[]> SignIntentAsync(byte[] serializedIntent);
    string GetAddress();
}

LocalDevSigner (Development Only)

āš ļø WARNING: LocalDevSigner stores private keys locally. Never use for production player wallets.

// Create new random keypair
var signer = await LocalDevSigner.CreateNewAsync("secure_password");

// Or import from mnemonic
var signer = await LocalDevSigner.ImportFromMnemonicAsync(
    "your twelve word mnemonic phrase here...",
    "secure_password"
);

string address = signer.GetAddress();
byte[] signature = await signer.SignIntentAsync(intentBytes);

// Lock when done to clear memory
signer.Lock();

ExternalWalletSigner (Production Recommended)

For production games, integrate with natos-wallet:

// Mobile deep link flow (future implementation)
var signer = new ExternalWalletSigner(playerAddress);

// This will open natos-wallet app for signing
var result = await _client.SendIntentAsync(intent, signer);

Integration Flow (Mobile):

  1. Serialize intent to base64
  2. Create deep link: rivellum://sign?intent=<base64>&callback=<your_scheme>
  3. Open link with Application.OpenURL(deepLink)
  4. Handle callback in Unity with signed intent
  5. Submit signed intent to network

Status: Stub implementation. Full natos-wallet integration pending API spec.


Querying Balances

Get Account Balance

async Task<BalanceResponse> GetPlayerBalance(string address)
{
    var balance = await _client.GetBalanceAsync(address);
    
    // Get RIVL balance
    ulong rivl = balance.GetRivlBalance();
    
    // Get specific asset
    ulong tokens = balance.GetAssetBalance("0x_custom_token_id_");
    
    // Current nonce (for sending intents)
    ulong nonce = balance.Nonce;
    
    return balance;
}

Display in UI

public Text balanceText;
public Text addressText;

async void RefreshBalance()
{
    var balance = await GetPlayerBalance(playerAddress);
    
    addressText.text = $"Address: {ShortenAddress(playerAddress)}";
    balanceText.text = $"Balance: {FormatRivl(balance.GetRivlBalance())} RIVL";
}

string ShortenAddress(string addr) 
    => $"{addr.Substring(0, 6)}...{addr.Substring(addr.Length - 4)}";

string FormatRivl(ulong microRivl) 
    => (microRivl / 1_000_000.0).ToString("F2");

Building Intents

IntentBuilder Fluent API

var intent = new IntentBuilder()
    .ForSender(senderAddress)
    .WithNonce(currentNonce)
    .WithMaxFee(2_000_000) // 2 RIVL max fee
    
    // Add actions
    .Transfer(recipientAddress, 1_000_000, "RIVL")
    .CallContract("GameRewards", "claim_daily", new { player_id = 123 })
    .MintNft("my_collection", new Dictionary<string, object>
    {
        ["name"] = "Legendary Sword",
        ["image"] = "https://cdn.example.com/sword.png",
        ["attributes"] = new { power = 100, rarity = "legendary" }
    })
    
    .Build();

Common Intent Patterns

Transfer Tokens

var intent = new IntentBuilder()
    .ForSender(playerAddress)
    .WithNonce(nonce)
    .Transfer(merchantAddress, 5_000_000, "RIVL") // 5 RIVL
    .Build();

Call Game Contract

var intent = new IntentBuilder()
    .ForSender(playerAddress)
    .WithNonce(nonce)
    .CallContract("GameRewards", "claim_reward", new 
    {
        quest_id = "dragon_slayer_01",
        reward_type = "gold",
        amount = 1000
    })
    .Build();

Mint Player NFT

var intent = new IntentBuilder()
    .ForSender(playerAddress)
    .WithNonce(nonce)
    .MintNft("player_items", new Dictionary<string, object>
    {
        ["name"] = "Epic Shield",
        ["description"] = "Forged in dragon fire",
        ["image"] = "ipfs://QmAbCdEf...",
        ["attributes"] = new
        {
            defense = 75,
            durability = 100,
            enchantment = "fire_resistance"
        }
    })
    .Build();

Transfer NFT

var intent = new IntentBuilder()
    .ForSender(playerAddress)
    .WithNonce(nonce)
    .TransferNft("nft_id_here", recipientAddress)
    .Build();

Simulating Intents

Why Simulate?

Simulation predicts:

  • āœ… Will the intent succeed or fail?
  • āœ… How much will it cost in fees?
  • āœ… What state changes will occur?

Simulate Before Sending

async Task<bool> TrySimulate(Intent intent)
{
    var simulation = await _client.SimulateIntentAsync(intent);
    
    if (simulation.Success)
    {
        Debug.Log($"Simulation passed! Estimated fee: {simulation.EstimatedFee} micro-RIVL");
        return true;
    }
    else
    {
        Debug.LogWarning($"Simulation failed: {simulation.ErrorMessage}");
        return false;
    }
}

Show Fee Estimate to Player

public Text feeEstimateText;

async void ShowFeeEstimate(Intent intent)
{
    var simulation = await _client.SimulateIntentAsync(intent);
    
    if (simulation.Success)
    {
        float feeRivl = simulation.EstimatedFee / 1_000_000f;
        feeEstimateText.text = $"Estimated fee: {feeRivl:F4} RIVL";
    }
}

Sending Intents

Method 1: Auto-Simulation (Recommended)

// Config has AutoSimulateBeforeSend = true by default
var result = await _client.SendIntentAsync(intent, signer);
// Automatically simulates first, throws if prediction is failure

Method 2: Manual Simulation + Send

var simulation = await _client.SimulateIntentAsync(intent);

if (simulation.Success)
{
    var result = await _client.SendIntentAsync(intent, signer);
    Debug.Log($"Sent! Intent ID: {result.IntentId}");
}

Method 3: Combined SimulateAndSend

var result = await _client.SimulateAndSendAsync(
    intent, 
    signer, 
    requireSuccessPrediction: true
);

if (result.WasSent)
{
    Debug.Log($"Intent sent! ID: {result.SendResult.IntentId}");
    Debug.Log($"Fee: {result.Simulation.EstimatedFee} micro-RIVL");
}
else
{
    Debug.LogWarning($"Not sent: {result.Simulation.ErrorMessage}");
}

Error Handling

try
{
    var result = await _client.SendIntentAsync(intent, signer);
}
catch (RivellumRpcException ex)
{
    Debug.LogError($"RPC Error: {ex.Message}");
    Debug.LogError($"Error Code: {ex.ErrorCode}");
    Debug.LogError($"Node Message: {ex.NodeMessage}");
}

Event Subscriptions

Subscribe to Events

using Rivellum.Unity.Models;

void SubscribeToPlayerEvents(string playerAddress)
{
    var filter = new EventFilter
    {
        Addresses = new List<string> { playerAddress },
        EventTypes = new List<string> { "transfer", "nft_minted" },
        FromHeight = 0 // Start from latest
    };

    _subscription = _client.SubscribeEvents(filter, OnEvent);
}

void OnEvent(RivellumEvent evt)
{
    Debug.Log($"Event: {evt.Type} at height {evt.Height}");
    
    if (evt.Type == "transfer")
    {
        var amount = evt.Data["amount"];
        ShowNotification($"Received {amount} RIVL!");
    }
    else if (evt.Type == "nft_minted")
    {
        var nftId = evt.Data["nft_id"];
        RefreshPlayerInventory();
    }
}

void OnDestroy()
{
    _subscription?.Dispose(); // Stop receiving events
}

Filter by Event Type

// Only subscribe to NFT events
var filter = new EventFilter
{
    EventTypes = new List<string> 
    { 
        "nft_minted", 
        "nft_transferred", 
        "nft_burned" 
    }
};

_subscription = _client.SubscribeEvents(filter, evt => 
{
    Debug.Log($"NFT Event: {evt.Type}, NFT ID: {evt.Data["nft_id"]}");
});

Cancel Subscription

// Option 1: Dispose
_subscription.Dispose();

// Option 2: Cancel
_subscription.Cancel();

// Option 3: Using statement (auto-disposes)
using (var sub = _client.SubscribeEvents(filter, OnEvent))
{
    // Subscription active within this scope
    await Task.Delay(60000); // Listen for 1 minute
} // Auto-cancelled here

Mobile Build Settings

iOS

  1. Network Permissions: Add to Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
  1. URL Scheme (for external wallet integration):
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourgame</string>
        </array>
    </dict>
</array>

Android

  1. Network Permission: Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  1. Clear Text Traffic (for local devnet testing):
<application android:usesCleartextTraffic="true" ... >
  1. Deep Links: Add intent filter to main activity:
<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="yourgame" />
</intent-filter>

Unity Player Settings

  • Api Compatibility Level: .NET Standard 2.1
  • Scripting Backend: IL2CPP (recommended for mobile)
  • Target Architectures: ARM64 (iOS), ARM64 + ARMv7 (Android)
  • Minimum iOS Version: 13.0+
  • Minimum Android API: 24+

Sample Scenes

1. Wallet Card Demo

Location: rivellum-unity-sdk/examples/Unity/RivellumWalletDemo/

Features:

  • Display player address (shortened)
  • Show RIVL balance with refresh button
  • List recent intents/transactions
  • Simple send RIVL UI

Key Scripts:

  • WalletCardController.cs - Main controller
  • BalanceDisplay.cs - Format and update balance UI
  • TransactionHistoryItem.cs - Recent activity list item

Setup:

  1. Open scene in Unity 6.x
  2. Configure node URL in WalletCardController inspector
  3. Play in editor or build to device
  4. Connect to devnet and view wallet

2. Game Rewards Demo

Location: rivellum-unity-sdk/examples/Unity/RivellumGameRewardsDemo/

Features:

  • Query GameRewards contract for player points
  • Display earned rewards
  • Claim rewards via contract call
  • Simulate before claiming

Key Scripts:

  • GameRewardsController.cs - Contract interaction
  • RewardClaimButton.cs - Claim with simulation
  • PointsDisplay.cs - Show player points

Setup:

  1. Open scene in Unity 6.x
  2. Deploy GameRewards contract to devnet (see contract examples)
  3. Configure contract address in inspector
  4. Play and interact with rewards system

Best Practices

1. Always Use Async/Await Properly

// āœ… Good - proper async handling
async void Start()
{
    try
    {
        var balance = await _client.GetBalanceAsync(address);
        UpdateUI(balance);
    }
    catch (Exception ex)
    {
        Debug.LogError($"Failed to fetch balance: {ex}");
    }
}

// āŒ Bad - blocking the Unity thread
void Start()
{
    var balance = _client.GetBalanceAsync(address).Result; // Blocks!
}

2. Cache Balance Queries

private BalanceResponse _cachedBalance;
private float _lastBalanceUpdate;
private const float BALANCE_CACHE_SECONDS = 5f;

async Task<BalanceResponse> GetBalanceWithCache(string address)
{
    if (_cachedBalance != null && 
        Time.time - _lastBalanceUpdate < BALANCE_CACHE_SECONDS)
    {
        return _cachedBalance;
    }

    _cachedBalance = await _client.GetBalanceAsync(address);
    _lastBalanceUpdate = Time.time;
    return _cachedBalance;
}

3. Handle Network Errors Gracefully

async Task<BalanceResponse?> SafeGetBalance(string address)
{
    try
    {
        return await _client.GetBalanceAsync(address);
    }
    catch (RivellumRpcException ex)
    {
        ShowErrorToPlayer($"Network error: {ex.Message}");
        return null;
    }
}

4. Dispose Resources Properly

public class RivellumManager : MonoBehaviour
{
    private RivellumClient _client;
    private EventSubscription _subscription;

    void OnDestroy()
    {
        _subscription?.Dispose();
        _client?.Dispose();
    }
}

5. Never Store Private Keys in PlayerPrefs

// āŒ BAD - insecure!
PlayerPrefs.SetString("private_key", privateKey);

// āœ… GOOD - use external wallet for production
var signer = new ExternalWalletSigner(playerAddress);

// āœ… OK - dev signer with warning labels
#if UNITY_EDITOR
var signer = await LocalDevSigner.CreateNewAsync("dev_password");
// NOTE: LocalDevSigner is DEV ONLY, use external wallet in production
#endif

Troubleshooting

Build Errors

Error: The type or namespace 'Newtonsoft' could not be found

Solution: Install Newtonsoft.Json via Package Manager:

com.unity.nuget.newtonsoft-json

Error: Task not found

Solution: Ensure Api Compatibility Level is set to .NET Standard 2.1 or .NET 4.x.

Runtime Errors

Error: RivellumRpcException: Connection refused

Solution: Verify node URL is correct and node is running:

var config = new RivellumClientConfig 
{ 
    NodeUrl = "http://localhost:8080" // Check this matches your node
};

Error: Simulation failed: Insufficient balance

Solution: Ensure sender has enough RIVL for amount + fees:

var balance = await _client.GetBalanceAsync(senderAddress);
if (balance.GetRivlBalance() < amount + estimatedFee)
{
    Debug.LogWarning("Insufficient balance for transaction");
}

Error: Intent signature verification failed

Solution: Ensure nonce is correct and signer address matches intent sender:

var balance = await _client.GetBalanceAsync(senderAddress);
var intent = new IntentBuilder()
    .ForSender(senderAddress)
    .WithNonce(balance.Nonce) // Use current nonce
    .Transfer(...)
    .Build();

Mobile Issues

iOS: App crashes when making HTTP requests

Solution: Add App Transport Security exception to Info.plist (see Mobile Build Settings).

Android: Network calls timeout

Solution: Add INTERNET permission to AndroidManifest.xml (see Mobile Build Settings).


Next Steps


Questions or Issues?