Skip to main content

Wallet Plugins

The Monolythium Desktop Wallet includes a plugin system (v0.2.0) that extends wallet functionality with modular features. Plugins are first-party only -- they are developed by the Monolythium team, bundled with the wallet, and cannot be loaded from external sources.


Overview

PropertyDetails
Plugin System Versionv0.2.0
Plugin SourceFirst-party only (bundled with wallet)
State ManagementZustand + Tauri persistence
Permission ModelConsent-based (advisory), not runtime-enforced
Discovery/services marketplace page in the wallet

The plugin system is designed for extensibility within a controlled trust boundary. Since all plugins are developed by the Monolythium team, the permission system serves as a transparency mechanism rather than a strict sandbox.


Plugin Architecture

Each plugin consists of three components:

  1. Manifest (manifest.ts) -- Declares the plugin's metadata, permissions, and entry point.
  2. Component folder -- Contains the React components, hooks, and logic for the plugin's UI.
  3. Registry entry -- The plugin is registered in src/plugins/registry.ts to make it available to the wallet.

File Structure

src/plugins/
├── registry.ts # Central plugin registry
├── validator-staking/
│ ├── manifest.ts # Plugin metadata and permissions
│ ├── ValidatorStakingPlugin.tsx
│ ├── components/
│ │ ├── DelegateForm.tsx
│ │ ├── UnbondForm.tsx
│ │ ├── ClaimRewards.tsx
│ │ └── ValidatorList.tsx
│ ├── hooks/
│ │ └── useStaking.ts
│ └── store.ts # Zustand store with Tauri persistence
├── nft-gallery/ # (Planned)
│ ├── manifest.ts
│ └── ...

Manifest

The manifest file declares everything the wallet needs to know about a plugin before loading it:

// src/plugins/validator-staking/manifest.ts
import type { PluginManifest } from '@/types/plugins';

export const manifest: PluginManifest = {
id: 'validator-staking',
name: 'Validator Staking',
description: 'Delegate, redelegate, unbond, and claim staking rewards from validators.',
version: '1.0.0',
author: 'Monolythium',
icon: 'shield-check',
permissions: [
'network:read',
'wallet:read',
'wallet:sign',
],
entryComponent: 'ValidatorStakingPlugin',
};

Registry

All plugins must be registered in the central registry to appear in the wallet:

// src/plugins/registry.ts
import { manifest as validatorStaking } from './validator-staking/manifest';

export const pluginRegistry = [
validatorStaking,
// Add new plugins here
];

Permission Types

The plugin system defines six permission types. Each plugin declares the permissions it requires in its manifest, and the user is shown a consent dialog when enabling the plugin.

PermissionScopeDescription
network:readRead-onlyQuery blockchain state (balances, staking info, contract data)
network:writeWriteBroadcast transactions to the network
wallet:readRead-onlyAccess wallet addresses and public keys
wallet:signWriteRequest transaction signing (triggers password prompt)
storage:readRead-onlyRead plugin-specific persistent storage
storage:writeWriteWrite to plugin-specific persistent storage
Consent-Based Permissions

The permission system is advisory, not runtime-enforced. When a user enables a plugin, they see the list of requested permissions and consent to them. Since all plugins are first-party and bundled with the wallet binary, there is no technical sandbox preventing a plugin from accessing APIs beyond its declared permissions. The permission declarations serve as a transparency contract -- they tell the user exactly what the plugin intends to do.

Permission Dialog

When a user enables a plugin from the /services page, they see a dialog listing the plugin's requested permissions:

Enable "Validator Staking"?

This plugin requests the following permissions:
- Read network state (balances, staking info)
- Read wallet addresses
- Sign transactions (will prompt for password)

[Cancel] [Enable]

State Management

Plugins use Zustand for state management with Tauri persistence for data that must survive app restarts.

Zustand Store

Each plugin defines its own Zustand store. The store is scoped to the plugin and does not interfere with the wallet's core state.

// src/plugins/validator-staking/store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { tauriStorage } from '@/lib/tauri-storage';

interface StakingState {
selectedValidator: string | null;
delegations: Delegation[];
rewards: Reward[];
setSelectedValidator: (address: string | null) => void;
setDelegations: (delegations: Delegation[]) => void;
setRewards: (rewards: Reward[]) => void;
}

export const useStakingStore = create<StakingState>()(
persist(
(set) => ({
selectedValidator: null,
delegations: [],
rewards: [],
setSelectedValidator: (address) => set({ selectedValidator: address }),
setDelegations: (delegations) => set({ delegations }),
setRewards: (rewards) => set({ rewards }),
}),
{
name: 'plugin-validator-staking',
storage: createJSONStorage(() => tauriStorage),
}
)
);

Tauri Persistence

The tauriStorage adapter writes plugin state to the Tauri app data directory on disk. This ensures plugin preferences and cached data persist across wallet restarts. Each plugin's storage is namespaced by its id to prevent key collisions.


Services Marketplace

The /services page in the Desktop Wallet serves as the plugin marketplace. It displays all available plugins from the registry with their:

  • Name, description, and icon
  • Version number
  • Permission requirements
  • Enable/disable toggle

Users can browse available plugins, review their permissions, and enable or disable them. Enabled plugins appear in the wallet's sidebar navigation.


First Plugin: Validator Staking

The Validator Staking plugin is the first plugin shipped with the Desktop Wallet. It provides a full staking management interface.

Features

FeatureDescription
DelegateStake LYTH to a validator from the validator browser
RedelegateMove staked tokens between validators without unbonding
UnbondBegin the unbonding process (3-day unbonding period on Monolythium)
Claim RewardsWithdraw accumulated staking rewards
Validator BrowserSearchable table with moniker, voting power, commission, and status

Password-Gated Signing

All staking transactions require password authentication before signing. The plugin calls the wallet's native signing API, which:

  1. Prompts the user for their vault password.
  2. Derives the encryption key using Argon2id (64 MB memory cost, 3 iterations, 4 parallel lanes).
  3. Decrypts the private key from the AES-256-GCM encrypted vault.
  4. Signs the transaction.
  5. Zeroizes the key material from memory immediately after signing.

This flow is identical to the password gate used for send transactions in the core wallet. The Rust backend handles all cryptographic operations -- the plugin's TypeScript code never has access to raw private keys.

Password Required for Every Transaction

Every delegate, redelegate, unbond, and claim operation requires the user to re-enter their password. There is no "remember password" option for staking operations. This is a deliberate security measure.


How to Add a New Plugin

Follow these steps to add a new first-party plugin to the Desktop Wallet.

Step 1: Create the Plugin Directory

Create a new directory under src/plugins/ with your plugin's ID:

src/plugins/my-plugin/
├── manifest.ts
├── MyPlugin.tsx
├── components/
├── hooks/
└── store.ts

Step 2: Define the Manifest

Create manifest.ts declaring your plugin's metadata and permissions:

import type { PluginManifest } from '@/types/plugins';

export const manifest: PluginManifest = {
id: 'my-plugin',
name: 'My Plugin',
description: 'A description of what this plugin does.',
version: '1.0.0',
author: 'Monolythium',
icon: 'puzzle-piece', // Lucide icon name
permissions: [
'network:read',
'wallet:read',
// Add only the permissions your plugin needs
],
entryComponent: 'MyPlugin',
};

Step 3: Build the Entry Component

Create the main React component that will be rendered when the plugin is active:

// src/plugins/my-plugin/MyPlugin.tsx
import { useMyPluginStore } from './store';

export function MyPlugin() {
const { data, setData } = useMyPluginStore();

return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">My Plugin</h2>
{/* Plugin UI */}
</div>
);
}

Step 4: Create a Zustand Store (Optional)

If your plugin needs persistent state:

// src/plugins/my-plugin/store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { tauriStorage } from '@/lib/tauri-storage';

interface MyPluginState {
data: string[];
setData: (data: string[]) => void;
}

export const useMyPluginStore = create<MyPluginState>()(
persist(
(set) => ({
data: [],
setData: (data) => set({ data }),
}),
{
name: 'plugin-my-plugin',
storage: createJSONStorage(() => tauriStorage),
}
)
);

Step 5: Register the Plugin

Add your plugin's manifest to src/plugins/registry.ts:

import { manifest as validatorStaking } from './validator-staking/manifest';
import { manifest as myPlugin } from './my-plugin/manifest';

export const pluginRegistry = [
validatorStaking,
myPlugin,
];

Step 6: Build and Test

pnpm tauri dev

Navigate to /services in the wallet to see your plugin listed. Enable it and verify it appears in the sidebar.

Follow the Validator Staking Pattern

The Validator Staking plugin is the reference implementation. When building a new plugin, follow the same patterns for component structure, store setup, and Tauri API usage. This ensures consistency across the plugin ecosystem.


Security Model

First-Party Only

The Desktop Wallet does not support loading third-party plugins. All plugins are:

  • Developed by the Monolythium team
  • Compiled into the wallet binary at build time
  • Reviewed and tested as part of the wallet's release process

There is no plugin download mechanism, no external plugin repository, and no way to side-load plugins at runtime.

No Third-Party Execution

The plugin system intentionally lacks the infrastructure for third-party code execution:

  • No plugin loader or dynamic import from external URLs
  • No plugin sandboxing or process isolation (unnecessary for first-party code)
  • No plugin signing or verification (the wallet binary itself is signed)

Tauri Security Boundary

All plugins run within the Tauri webview, which enforces:

  • Content Security Policy -- Restricts network connections to *.mononodes.xyz and GitHub (for auto-update checks)
  • No Node.js access -- Plugins cannot spawn child processes or access the filesystem directly
  • IPC commands -- Plugins interact with the Rust backend exclusively through Tauri's typed IPC command system

Private key operations are handled entirely in the Rust backend. The TypeScript layer (where plugins run) never has access to raw key material.


Planned Plugins

PluginStatusDescription
Validator StakingShipped (v1.0.0)Full staking management
NFT GalleryPlannedBrowse and manage ERC-721 NFTs
DeFi DashboardPlannedLP positions, farming rewards, swap history
Agent ManagerPlannedRegister and monitor on-chain agents