RISE Logo-Light

Viem Integration

Using RISE Wallet with Viem for low-level control

This guide covers integrating RISE Wallet with Viem, a TypeScript interface for Ethereum that provides low-level access to blockchain functionality.

Overview

RISE Wallet provides first-class Viem support through custom actions and account implementations. This integration is ideal for developers who need fine-grained control over wallet operations.

Installation

npm i rise-wallet viem
pnpm add rise-wallet viem
yarn add rise-wallet viem
bun add rise-wallet viem

Basic Usage

Creating a Client

Use RISE Wallet's EIP-1193 provider with Viem's custom transport:

import { createClient, custom } from "viem";
import { RiseWallet } from "rise-wallet";

// Create RISE Wallet instance
const rw = RiseWallet.create();

// Create Viem client with RISE Wallet provider
const client = createClient({
  transport: custom(rw.provider),
});

Using Wallet Actions

RISE Wallet exports a WalletActions module with RISE Wallet-specific functionality:

import { WalletActions } from "rise-wallet/viem";

// Connect to wallet
const { accounts } = await WalletActions.connect(client);
console.log("Connected account:", accounts[0]);

// Get user assets
const assets = await WalletActions.getAssets(client);
console.log("User assets:", assets);

Standard Viem Actions

All standard Viem wallet actions work with RISE Wallet:

Send Transaction

import { parseEther } from "viem";
import * as Actions from "viem/actions";

const hash = await Actions.sendTransaction(client, {
  to: "0x...",
  value: parseEther("0.001"),
});

console.log("Transaction hash:", hash);

Send Calls (EIP-5792)

const result = await Actions.sendCalls(client, {
  account: "0x...",
  calls: [
    {
      to: "0x...",
      data: "0x...",
    },
  ],
});

Sign Message

const signature = await Actions.signMessage(client, {
  account: "0x...",
  message: "Hello RISE!",
});

RISE Wallet Actions

Grant Permissions

Create session keys with specific permissions:

import { Key } from "rise-wallet/viem";
import { keccak256, toHex, parseEther } from "viem";

const permissions = await WalletActions.grantPermissions(client, {
  key: Key.createP256(), // Generate new P256 key
  expiry: Math.floor(Date.now() / 1000) + 3600, // 1 hour
  permissions: {
    calls: [
      {
        to: "0x...", // Contract address
        signature: keccak256(toHex("transfer(address,uint256)")).slice(0, 10),
      }
    ],
    spend: [
      {
        limit: parseEther("50"),
        period: "minute",
        token: "0x...",
      }
    ],
  },
});

Get Permissions

View active permissions:

const permissions = await WalletActions.getPermissions(client);

permissions.forEach(permission => {
  console.log("Key:", permission.key);
  console.log("Expiry:", permission.expiry);
  console.log("Allowed calls:", permission.calls);
});

Revoke Permissions

Remove granted permissions:

await WalletActions.revokePermissions(client, {
  keyId: "0x...",
});

Key Management

RISE Wallet provides utilities for working with different key types:

P256 Keys

import { Key, PublicKey } from "rise-wallet/viem";

// Create new P256 key
const key = Key.createP256();

// Import existing key
const imported = Key.fromP256({
  privateKey: "0x...",
});

// Sign with key
const signature = await Key.sign({
  payload: "0x...",
  privateKey: key.privateKey,
});

WebAuthn Keys

// Create WebAuthn credential
const credential = await Key.createWebAuthnP256({
  user: {
    name: "alice@example.com",
    displayName: "Alice",
  },
});

// Use for signing
const webAuthnKey = Key.fromWebAuthnP256({
  credential,
  publicKey: credential.publicKey,
});

Advanced Patterns

Direct Provider Access

For maximum control, access the provider directly:

const provider = await rw.provider;

// Use JSON-RPC methods directly
const result = await provider.request({
  method: "wallet_prepareCalls",
  params: [{
    calls: [{
      to: "0x...",
      data: "0x...",
    }],
    chainId: "0x...",
    from: "0x...",
  }],
});

Session Key Signing

Sign transactions with session keys:

import { P256, Signature } from "ox";

// Prepare transaction
const { digest, ...request } = await provider.request({
  method: "wallet_prepareCalls",
  params: [{
    calls: [...],
    key: { publicKey, type: "p256" },
  }],
});

// Sign with session key
const signature = Signature.toHex(
  P256.sign({ payload: digest, privateKey })
);

// Send signed transaction
const result = await provider.request({
  method: "wallet_sendPreparedCalls",
  params: [{ ...request, signature }],
});

// Get the call bundle ID
const bundleId = Array.isArray(result) ? result[0].id : result.id;

// Poll for transaction status
const status = await provider.request({
  method: "wallet_getCallsStatus",
  params: [bundleId],
});

if (status.status === 200) {
  console.log("Transaction confirmed!");
  console.log("TX hash:", status.receipts[0].transactionHash);
} else {
  console.error("Transaction failed with status:", status.status);
}

Custom Actions

Create your own actions by extending Viem:

import { type Client } from "viem";

export async function myCustomAction(client: Client, params: any) {
  return client.request({
    method: "my_custom_method",
    params,
  });
}

// Use with client.extend()
const extendedClient = client.extend(() => ({
  myCustomAction,
}));

await extendedClient.myCustomAction({ ... });

WebSocket Support

For realtime updates, use WebSocket transport:

import { createPublicClient, webSocket } from "viem";
import { riseTestnet } from "viem/chains";

const wsClient = createPublicClient({
  chain: riseTestnet,
  transport: webSocket("wss://testnet.riselabs.xyz/ws"),
});

// Watch for events in realtime
wsClient.watchContractEvent({
  address: "0x...",
  abi: [...],
  eventName: "Transfer",
  onLogs: (logs) => {
    console.log("Transfer events:", logs);
  },
});

Type Safety

RISE Wallet provides comprehensive TypeScript types:

import type { PermissionsRequest } from "rise-wallet/viem";

// All actions are fully typed
const permissions: PermissionsRequest = {
  key: { publicKey: "...", type: "p256" },
  expiry: 123456789,
  permissions: {
    calls: [{
      to: "0x..." as const,
      signature: "0x..." as const,
    }],
  },
};

Polling Transaction Status

After sending prepared calls, you need to poll for the transaction status using wallet_getCallsStatus:

// Send prepared calls
const result = await provider.request({
  method: "wallet_sendPreparedCalls",
  params: [{ ...preparedRequest, signature }],
});

// Extract bundle ID
const bundleId = Array.isArray(result) ? result[0].id : result.id;

// Poll for status
const status = await provider.request({
  method: "wallet_getCallsStatus",
  params: [bundleId],
});

Status Codes

The status field provides a summary of the current bundle status:

CodeDescription
100Pending - Bundle received but not executed onchain yet
200Confirmed - Bundle included onchain without reverts, receipts available
300Failed (Offchain) - Bundle not included onchain, wallet will not retry
400Reverted (Complete) - Bundle completely reverted, only gas charges may be onchain
500Reverted (Partial) - Bundle partially reverted, some changes may be onchain

Status Response

type GetCallsStatusResponse = {
  id: string,                    // Bundle ID
  status: number,                // Status code (100-500)
  receipts: {
    logs: {
      chainId: string,
      address: string,
      data: string,
      topics: string[],
    }[],
    status: string,              // "0x1" for success, "0x0" for failure
    blockHash?: string,
    blockNumber?: string,
    gasUsed: string,
    transactionHash: string,     // The actual transaction hash
  }[],
  capabilities?: {
    interopStatus?: 'pending' | 'confirmed' | 'failed',
  },
}

Example: Complete Flow with Polling

import { P256, Signature } from "ox";

async function sendWithSessionKey(calls: any[], sessionKey: string) {
  const provider = await rw.provider;

  // 1. Prepare the calls
  const { digest, ...request } = await provider.request({
    method: "wallet_prepareCalls",
    params: [{
      calls,
      chainId: "0x...",
      from: "0x...",
      key: { publicKey: sessionKey, type: "p256" },
    }],
  });

  // 2. Sign with session key
  const signature = Signature.toHex(
    P256.sign({ payload: digest, privateKey: "0x..." })
  );

  // 3. Send signed calls
  const result = await provider.request({
    method: "wallet_sendPreparedCalls",
    params: [{ ...request, signature }],
  });

  // 4. Get bundle ID
  const bundleId = Array.isArray(result) ? result[0].id : result.id;

  // 5. Poll for status (simplified - you may want to add retries/timeouts)
  const status = await provider.request({
    method: "wallet_getCallsStatus",
    params: [bundleId],
  });

  // 6. Check status
  if (status.status !== 200) {
    throw new Error(`Transaction failed with status ${status.status}`);
  }

  // 7. Return transaction hash
  return status.receipts[0].transactionHash;
}

Note: wallet_sendCalls and sendCallsSync handle polling internally. You only need to manually poll wallet_getCallsStatus when using wallet_sendPreparedCalls for session key transactions.

Error Handling

Handle Viem errors appropriately:

import { BaseError } from "viem";

try {
  await Actions.sendTransaction(client, { ... });
} catch (error) {
  if (error instanceof BaseError) {
    console.error("Error name:", error.name);
    console.error("Error details:", error.details);
    console.error("Error version:", error.version);
  }
}

Examples