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 viempnpm add rise-wallet viemyarn add rise-wallet viembun add rise-wallet viemBasic 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:
| Code | Description |
|---|---|
| 100 | Pending - Bundle received but not executed onchain yet |
| 200 | Confirmed - Bundle included onchain without reverts, receipts available |
| 300 | Failed (Offchain) - Bundle not included onchain, wallet will not retry |
| 400 | Reverted (Complete) - Bundle completely reverted, only gas charges may be onchain |
| 500 | Reverted (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);
}
}