RISE Logo-Light

Wagmi Integration

Using RISE Wallet with Wagmi React hooks

This guide covers integrating RISE Wallet with Wagmi, the popular collection of React Hooks for Ethereum.

Overview

RISE Wallet implements a Wagmi connector and provides custom React hooks that map directly to the wallet's capabilities. The integration is designed to feel natural to developers already familiar with Wagmi.

Installation

npm i rise-wallet wagmi viem @tanstack/react-query
pnpm add rise-wallet wagmi viem @tanstack/react-query
yarn add rise-wallet wagmi viem @tanstack/react-query
bun add rise-wallet wagmi viem @tanstack/react-query

Basic Setup

1. Configure the RISE Wallet Connector

config/wagmi.ts
import { Chains, RiseWallet } from "rise-wallet";
import { riseWallet } from "rise-wallet/wagmi";
import { createConfig, http } from "wagmi";

// Export the connector for advanced usage
export const rwConnector = riseWallet(RiseWallet.defaultConfig);

// Create wagmi config
export const config = createConfig({
  chains: [Chains.riseTestnet],
  connectors: [rwConnector],
  transports: {
    [Chains.riseTestnet.id]: http("https://testnet.riselabs.xyz"),
  },
});

2. Set Up Providers

app/providers.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { config } from "@/config/wagmi";
import { useState } from "react";

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());

  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </WagmiProvider>
  );
}

3. Wrap Your App in the Provider

Add the Providers component to your root layout:

app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Standard Wagmi Hooks

All standard Wagmi hooks work seamlessly with RISE Wallet:

Connection Management

components/WalletButton.tsx
import { useConnect, useConnection, useDisconnect, useConnectors } from "wagmi";

export function WalletButton() {
  const { address, isConnected } = useConnection();
  const connect = useConnect();
  const disconnect = useDisconnect();
  const connectors = useConnectors();

  if (isConnected) {
    return (
      <div>
        <span>{address}</span>
        <button onClick={() => disconnect.mutate()}>Disconnect</button>
      </div>
    );
  }

  const rwConnector = connectors.find(c => c.id === "com.risechain.wallet");
  if (!rwConnector) return null;

  return (
    <button onClick={() => connect.mutate({ connector: rwConnector })}>
      Connect with Passkey
    </button>
  );
}

Sending Transactions

components/SendTransaction.tsx
import { useSendCalls } from "wagmi";
import { parseEther } from "viem";

export function SendTransaction() {
  const sendCalls = useSendCalls();
  
  //for sending 0.001 ETH to some address
  const handleSend = async () => {
    const result = await sendCalls.mutateAsync({
      calls: [{
        to: "0x...",
        value: parseEther("0.001"),
      }],
    });

    console.log("Transaction sent:", result);
  };

  return (
    <button onClick={handleSend} disabled={sendCalls.isPending}>
      {sendCalls.isPending ? "Sending..." : "Send 0.001 ETH"}
    </button>
  );
}

RISE Wallet Specific Hooks

Import custom hooks via the Hooks namespace:

import { Hooks } from "rise-wallet/wagmi";

or individually:

import { useGrantPermissions } from "rise-wallet/wagmi/Hooks";

useGrantPermissions

Create session keys with specific permissions:

import { parseEther } from "viem";
import { P256, PublicKey } from "ox";

const grantPermissions = Hooks.useGrantPermissions();

const createSession = async () => {
  // 1. Generate a local key pair
  const privateKey = P256.randomPrivateKey();
  const publicKey = PublicKey.toHex(P256.getPublicKey({ privateKey }), {
    includePrefix: false,
  });

  // 2. Grant permissions to the session key
  await grantPermissions.mutateAsync({
    key: { publicKey, type: "p256" },
    expiry: Math.floor(Date.now() / 1000) + 3600, // 1 hour
    feeToken: null, // use native token for fees, or { limit: "0.01", symbol: "ETH" }

    // the permissions you want the session key to have
    permissions: {
      calls: [{ // function calls
        to: "0x...", // address of the contract on which the function is being called
        signature: "0x...", // function signature. you can get this via: cast sig "transfer(address,uint256)"
      }],
      
      // token spend limits
      spend: [{
        token: "0x...", // token contract address
        limit: parseEther("50"), // max amount (bigint)
        period: "minute", // time window (eg. "minute", "hour", "day")
      }],
    },
  });

  // 3. Store the private key to sign future transactions without popup
  localStorage.setItem("session_key", privateKey);
};

usePermissions

View current permissions for the connected account:

const { data: permissions } = Hooks.usePermissions();

if (permissions) {
  console.log("Active permissions:", permissions);
}

useRevokePermissions

Revoke previously granted permissions:

const revokePermissions = Hooks.useRevokePermissions();

const revokeSession = async (keyId: string) => {
  await revokePermissions.mutateAsync({
    keyId,
  });
};

useAssets

Get user's token balances and assets:

const { data: assets } = Hooks.useAssets();

return (
  <div>
    {assets?.map(asset => (
      <div key={asset.address}>
        {asset.symbol}: {asset.balance}
      </div>
    ))}
  </div>
);

Polling Transaction Status

When using session keys with wallet_sendPreparedCalls, you need to manually poll for transaction status using wallet_getCallsStatus:

import { P256, Signature } from "ox";

async function sendWithSessionKey(calls: any[], sessionPrivateKey: string, sessionPublicKey: string) {
  const provider = await connector.getProvider();

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

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

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

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

  // 5. Poll for transaction status
  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;
}

Status Codes

The status field in the response indicates the current state of the transaction bundle:

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

Response Structure

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',
  },
}

Note: Standard Wagmi hooks like useSendCalls handle polling internally using sendCallsSync. You only need to manually poll wallet_getCallsStatus when using session keys with wallet_sendPreparedCalls.

Advanced Patterns

Batched Transactions

Execute multiple operations atomically:

const sendCalls = useSendCalls();

// Approve and swap in one transaction
await sendCalls.mutateAsync({
  calls: [
    {
      to: TOKEN_ADDRESS,
      data: approveCalldata,
    },
    {
      to: DEX_ADDRESS,
      data: swapCalldata,
    },
  ],
  // Atomic execution - all succeed or all fail
  atomicRequired: true,
});

Reading Transaction Status

Monitor transaction progress:

import { useWaitForTransactionReceipt } from "wagmi";

const { data: receipt, isLoading } = useWaitForTransactionReceipt({
  hash: transactionHash,
});

if (receipt) {
  console.log("Transaction confirmed:", receipt);
}

Error Handling

Properly handle wallet errors:

import { BaseError } from "wagmi";

const sendCalls = useSendCalls();

try {
  await sendCalls.mutateAsync({...});
} catch (error) {
  if (error instanceof BaseError) {
    // Handle specific error types
    if (error.shortMessage.includes("rejected")) {
      console.log("User rejected the request");
    }
  }
}

TypeScript Support

RISE Wallet provides full TypeScript support:

import type { RiseWallet } from "rise-wallet";
import type { Config } from "wagmi";

// Config is fully typed
const config: Config = createConfig({
  chains: [Chains.riseTestnet],
  connectors: [rwConnector],
  transports: {
    [Chains.riseTestnet.id]: http(),
  },
});

Best Practices

1. Check Connection State

Always verify the connection state before attempting transactions:

const { isConnected } = useConnection();

if (!isConnected) {
  return <ConnectButton />;
}

2. Handle Loading States

Provide feedback during async operations:

const sendCalls = useSendCalls();
// Access: sendCalls.isPending, sendCalls.isSuccess, sendCalls.isError

3. Use Error Boundaries

Wrap your app with error boundaries to catch unexpected errors:

import { ErrorBoundary } from "react-error-boundary";

<ErrorBoundary fallback={<ErrorFallback />}>
  <App />
</ErrorBoundary>

Examples

For complete working examples, check out: