Skip to content

btc-vision/walletconnect

Repository files navigation

OP_NET - WalletConnect

Bitcoin TypeScript React NodeJS NPM

code style: prettier

Table of Contents

Introduction

The OP_NET WalletConnect library is a React-based TypeScript library that provides a unified interface for connecting Bitcoin wallets to your decentralized applications (dApps). It enables seamless wallet connections, transaction signing, balance retrieval, and network management through a simple React context and hooks API.

Built specifically for the OP_NET Bitcoin L1 smart contract ecosystem, this library supports quantum-resistant MLDSA signatures and provides automatic RPC provider configuration for OP_NET networks.

Features

  • Multi-Wallet Support: Connect to OP_WALLET and UniSat wallets with a unified API
  • React Integration: Easy-to-use React Provider and Hook pattern
  • Auto-Reconnect: Automatically reconnects to previously connected wallets
  • Theme Support: Built-in light, dark, and moto themes for the connection modal
  • Network Detection: Automatic network detection and switching support
  • MLDSA Signatures: Quantum-resistant ML-DSA signature support (OP_WALLET only)
  • Balance Tracking: Real-time wallet balance updates including CSV-locked amounts
  • TypeScript: Full TypeScript support with comprehensive type definitions
  • Browser & Node: Works in both browser and Node.js environments

Installation

Prerequisites

  • Node.js version 24.x or higher
  • React 19+
  • npm or yarn

Install via npm

npm install @btc-vision/walletconnect

Install via yarn

yarn add @btc-vision/walletconnect

Peer Dependencies

This library requires React 19+ as a peer dependency:

npm install react@^19 react-dom@^19

Quick Start

1. Wrap Your App with the Provider

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { WalletConnectProvider } from '@btc-vision/walletconnect';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <WalletConnectProvider theme="dark">
      <App />
    </WalletConnectProvider>
  </StrictMode>
);

2. Use the Hook in Your Components

import { useWalletConnect } from '@btc-vision/walletconnect';

function WalletButton() {
  const {
    openConnectModal,
    disconnect,
    walletAddress,
    publicKey,
    connecting,
    network,
  } = useWalletConnect();

  if (connecting) {
    return <button disabled>Connecting...</button>;
  }

  if (walletAddress) {
    return (
      <div>
        <p>Connected: {walletAddress}</p>
        <p>Network: {network?.network}</p>
        <button onClick={disconnect}>Disconnect</button>
      </div>
    );
  }

  return <button onClick={openConnectModal}>Connect Wallet</button>;
}

API Reference

WalletConnectProvider

The main provider component that wraps your application and provides wallet context to all child components.

Props

Prop Type Default Description
theme 'light' | 'dark' | 'moto' 'light' Theme for the connection modal
children ReactNode required Child components to render

Example

<WalletConnectProvider theme="dark">
  <App />
</WalletConnectProvider>

useWalletConnect Hook

The primary hook for accessing wallet state and methods. Must be used within a WalletConnectProvider.

Returns: WalletConnectContextType

const {
  // State
  allWallets,          // List of all supported wallets with installation status
  walletType,          // Current wallet type (e.g., 'OP_WALLET', 'UNISAT')
  walletAddress,       // Connected wallet's Bitcoin address
  walletInstance,      // Raw wallet instance for advanced operations
  network,             // Current network configuration
  publicKey,           // Connected wallet's public key (hex)
  address,             // Address object with MLDSA support
  connecting,          // Boolean indicating connection in progress
  provider,            // OP_NET RPC provider for blockchain queries
  signer,              // Transaction signer (UnisatSigner)
  walletBalance,       // Detailed wallet balance information
  mldsaPublicKey,      // MLDSA public key (OP_WALLET only)
  hashedMLDSAKey,      // SHA256 hash of MLDSA public key

  // Methods
  openConnectModal,    // Opens the wallet selection modal
  connectToWallet,     // Connects to a specific wallet
  disconnect,          // Disconnects from current wallet
  signMLDSAMessage,    // Signs a message with MLDSA (quantum-resistant)
  verifyMLDSASignature // Verifies an MLDSA signature
} = useWalletConnect();

State Properties

Property Type Description
allWallets WalletInformation[] Array of all supported wallets with their status
walletType string | null Identifier of the connected wallet type
walletAddress string | null Bitcoin address of the connected wallet
walletInstance Unisat | null Raw wallet instance for direct API calls
network WalletConnectNetwork | null Current network with chain type
publicKey string | null Public key of connected account (hex string)
address Address | null Address object combining publicKey and MLDSA key
connecting boolean True while connection is in progress
provider AbstractRpcProvider | null OP_NET JSON-RPC provider
signer UnisatSigner | null Signer for transaction signing
walletBalance WalletBalance | null Detailed balance breakdown
mldsaPublicKey string | null MLDSA public key for quantum-resistant signatures
hashedMLDSAKey string | null SHA256 hash of MLDSA public key

Methods

Method Signature Description
openConnectModal () => void Opens the wallet selection modal
connectToWallet (wallet: SupportedWallets) => void Connects directly to a specific wallet
disconnect () => void Disconnects from the current wallet
signMLDSAMessage (message: string) => Promise<MLDSASignature | null> Signs a message using MLDSA
verifyMLDSASignature (message: string, signature: MLDSASignature) => Promise<boolean> Verifies an MLDSA signature

WalletConnectContext

The raw React context for advanced use cases. Prefer using the useWalletConnect hook.

import { WalletConnectContext } from '@btc-vision/walletconnect';
import { useContext } from 'react';

const context = useContext(WalletConnectContext);

Types

WalletConnectNetwork

Extended network configuration with chain type information.

interface WalletConnectNetwork extends Network {
  chainType: UnisatChainType;  // Enum: BITCOIN_MAINNET, BITCOIN_TESTNET, BITCOIN_REGTEST
  network: string;              // Human-readable: 'mainnet', 'testnet', 'regtest'
}

WalletInformation

Information about a supported wallet.

interface WalletInformation {
  name: SupportedWallets;  // Wallet identifier
  icon: string;            // Base64 or URL of wallet icon
  isInstalled: boolean;    // Whether wallet extension is detected
  isConnected: boolean;    // Whether wallet is currently connected
}

WalletBalance

Detailed breakdown of wallet balance.

interface WalletBalance {
  total: number;              // Total balance in satoshis
  confirmed: number;          // Confirmed balance
  unconfirmed: number;        // Unconfirmed/pending balance
  csv75_total: number;        // Total CSV-75 locked amount
  csv75_unlocked: number;     // Unlocked CSV-75 amount
  csv75_locked: number;       // Currently locked CSV-75 amount
  csv1_total: number;         // Total CSV-1 locked amount
  csv1_unlocked: number;      // Unlocked CSV-1 amount
  csv1_locked: number;        // Currently locked CSV-1 amount
  p2wda_total_amount: number; // Total P2WDA amount
  p2wda_pending_amount: number; // Pending P2WDA amount
  usd_value: string;          // USD value as string
}

SupportedWallets

Enum of supported wallet types.

enum SupportedWallets {
  OP_WALLET = 'OP_WALLET',
  UNISAT = 'UNISAT',
}

Supported Wallets

OP_WALLET

The native OP_NET wallet with full feature support including MLDSA signatures.

Features:

  • Full OP_NET integration
  • MLDSA (quantum-resistant) signature support
  • Network switching
  • Account change detection

Installation: Chrome Web Store

UniSat

Popular Bitcoin wallet with broad ecosystem support.

Features:

  • Wide adoption
  • Network switching
  • Account change detection
  • Transaction signing via UnisatSigner

Limitations:

  • No MLDSA signature support

Installation: Chrome Web Store

Network Configuration

The library automatically configures OP_NET RPC providers based on the connected network:

Chain Type Network RPC Endpoint
BITCOIN_MAINNET mainnet https://mainnet.opnet.org
BITCOIN_TESTNET testnet https://testnet.opnet.org
BITCOIN_REGTEST regtest https://regtest.opnet.org

Using the Provider

const { provider, network } = useWalletConnect();

// Check current network
console.log(`Connected to: ${network?.network}`);

// Use provider for blockchain queries
if (provider) {
  const balance = await provider.getBalance('bc1q...');
  const blockNumber = await provider.getBlockNumber();
}

Theme Customization

The library includes three built-in themes for the connection modal:

Available Themes

Theme Description
light Light background with dark text
dark Dark background with light text
moto MotoSwap branded theme

Usage

// Light theme (default)
<WalletConnectProvider theme="light">

// Dark theme
<WalletConnectProvider theme="dark">

// Moto theme
<WalletConnectProvider theme="moto">

Custom Styling

The modal uses CSS classes that can be overridden:

/* Modal backdrop */
.wallet-connect-modal-backdrop { }

/* Modal container */
.wallet-connect-modal { }

/* Header */
.wallet-connect-header { }

/* Wallet list */
.wallet-list { }

/* Individual wallet button */
.wallet-button { }

/* Wallet icon */
.wallet-icon { }

/* Error message */
.wallet-connect-error { }

MLDSA Signatures

ML-DSA (Module-Lattice Digital Signature Algorithm) provides quantum-resistant cryptographic signatures. This feature is currently only available with OP_WALLET.

Checking MLDSA Support

const { mldsaPublicKey, walletType } = useWalletConnect();

const hasMLDSASupport = walletType === 'OP_WALLET' && mldsaPublicKey !== null;

Signing Messages

const { signMLDSAMessage, mldsaPublicKey } = useWalletConnect();

async function signMessage(message: string) {
  if (!mldsaPublicKey) {
    console.error('MLDSA not supported by current wallet');
    return;
  }

  const signature = await signMLDSAMessage(message);
  if (signature) {
    console.log('Signature:', signature);
  }
}

Verifying Signatures

const { verifyMLDSASignature } = useWalletConnect();

async function verify(message: string, signature: MLDSASignature) {
  const isValid = await verifyMLDSASignature(message, signature);
  console.log('Signature valid:', isValid);
}

Address with MLDSA

The address property combines both traditional public key and MLDSA public key:

const { address, publicKey, mldsaPublicKey } = useWalletConnect();

// address is created as:
// Address.fromString(mldsaPublicKey, publicKey)

Event Handling

The library automatically handles wallet events:

Account Changes

When the user switches accounts in their wallet, the library automatically updates:

  • walletAddress
  • publicKey
  • walletBalance

Network Changes

When the user switches networks:

  • network is updated
  • provider is reconfigured for the new network
  • Balance is refreshed

Disconnect

When the wallet disconnects:

  • All state is cleared
  • Local storage is cleaned
  • UI updates to disconnected state

Examples

Complete Connection Flow

import { useWalletConnect, SupportedWallets } from '@btc-vision/walletconnect';
import { useEffect, useState } from 'react';

function WalletManager() {
  const {
    openConnectModal,
    connectToWallet,
    disconnect,
    walletAddress,
    publicKey,
    network,
    walletBalance,
    provider,
    connecting,
    allWallets,
  } = useWalletConnect();

  // Check which wallets are installed
  const installedWallets = allWallets.filter(w => w.isInstalled);

  // Connect directly to a specific wallet
  const connectOP = () => connectToWallet(SupportedWallets.OP_WALLET);
  const connectUnisat = () => connectToWallet(SupportedWallets.UNISAT);

  if (connecting) {
    return <div>Connecting to wallet...</div>;
  }

  if (!walletAddress) {
    return (
      <div>
        <h2>Connect Your Wallet</h2>

        {/* Option 1: Open modal to choose */}
        <button onClick={openConnectModal}>
          Choose Wallet
        </button>

        {/* Option 2: Direct connection buttons */}
        <div>
          {installedWallets.map(wallet => (
            <button
              key={wallet.name}
              onClick={() => connectToWallet(wallet.name)}
            >
              Connect {wallet.name}
            </button>
          ))}
        </div>
      </div>
    );
  }

  return (
    <div>
      <h2>Wallet Connected</h2>
      <p><strong>Address:</strong> {walletAddress}</p>
      <p><strong>Public Key:</strong> {publicKey}</p>
      <p><strong>Network:</strong> {network?.network}</p>
      <p><strong>Balance:</strong> {walletBalance?.total} sats</p>
      <p><strong>USD Value:</strong> ${walletBalance?.usd_value}</p>
      <button onClick={disconnect}>Disconnect</button>
    </div>
  );
}

Using the Provider for Blockchain Queries

import { useWalletConnect } from '@btc-vision/walletconnect';
import { useEffect, useState } from 'react';

function BlockchainInfo() {
  const { provider, network } = useWalletConnect();
  const [blockNumber, setBlockNumber] = useState<number | null>(null);

  useEffect(() => {
    if (!provider) return;

    const fetchBlockNumber = async () => {
      try {
        const block = await provider.getBlockNumber();
        setBlockNumber(block);
      } catch (error) {
        console.error('Failed to fetch block number:', error);
      }
    };

    fetchBlockNumber();

    // Poll for updates
    const interval = setInterval(fetchBlockNumber, 10000);
    return () => clearInterval(interval);
  }, [provider]);

  if (!provider) {
    return <p>Connect wallet to view blockchain info</p>;
  }

  return (
    <div>
      <p>Network: {network?.network}</p>
      <p>Current Block: {blockNumber}</p>
    </div>
  );
}

Transaction Signing

import { useWalletConnect } from '@btc-vision/walletconnect';

function TransactionSigner() {
  const { signer, walletInstance, publicKey } = useWalletConnect();

  const signTransaction = async () => {
    if (!signer || !walletInstance) {
      console.error('Wallet not connected');
      return;
    }

    try {
      // Use the signer for OP_NET transactions
      // The signer handles interaction with the wallet
      console.log('Signer ready for transactions');
    } catch (error) {
      console.error('Transaction failed:', error);
    }
  };

  const signMessage = async (message: string) => {
    if (!walletInstance) return;

    try {
      const signature = await walletInstance.signMessage(message);
      console.log('Message signed:', signature);
      return signature;
    } catch (error) {
      console.error('Signing failed:', error);
    }
  };

  return (
    <div>
      <button onClick={() => signMessage('Hello OP_NET!')}>
        Sign Message
      </button>
    </div>
  );
}

MLDSA Quantum-Resistant Signing

import { useWalletConnect } from '@btc-vision/walletconnect';

function QuantumSafeSigning() {
  const {
    mldsaPublicKey,
    hashedMLDSAKey,
    signMLDSAMessage,
    verifyMLDSASignature,
    walletType,
  } = useWalletConnect();

  const [message, setMessage] = useState('');
  const [signature, setSignature] = useState<MLDSASignature | null>(null);

  const isMLDSASupported = walletType === 'OP_WALLET' && mldsaPublicKey;

  const handleSign = async () => {
    if (!message) return;

    const sig = await signMLDSAMessage(message);
    if (sig) {
      setSignature(sig);
      console.log('MLDSA Signature created');
    }
  };

  const handleVerify = async () => {
    if (!signature || !message) return;

    const isValid = await verifyMLDSASignature(message, signature);
    alert(isValid ? 'Signature is valid!' : 'Signature is invalid!');
  };

  if (!isMLDSASupported) {
    return <p>MLDSA signatures require OP_WALLET</p>;
  }

  return (
    <div>
      <p>MLDSA Public Key: {mldsaPublicKey?.slice(0, 20)}...</p>
      <p>Hashed Key: {hashedMLDSAKey}</p>

      <input
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="Message to sign"
      />

      <button onClick={handleSign}>Sign with MLDSA</button>

      {signature && (
        <button onClick={handleVerify}>Verify Signature</button>
      )}
    </div>
  );
}

Adding Custom Wallets

To add support for a new wallet, follow these steps:

1. Create Wallet Directory

Create a new directory in src/wallets/ for your wallet:

src/wallets/mywallet/
  ├── controller.ts
  └── interface.ts

2. Define the Interface

Create interface.ts with your wallet's browser API types:

// src/wallets/mywallet/interface.ts
import type { Unisat } from '@btc-vision/transaction';

export interface MyWalletInterface extends Unisat {
  // Add any wallet-specific methods
  customMethod(): Promise<string>;
}

// Export wallet icon (base64 or URL)
export const logo = 'data:image/svg+xml;base64,...';

3. Implement the Controller

Create controller.ts implementing the WalletBase interface:

// src/wallets/mywallet/controller.ts
import type { MLDSASignature, Unisat, UnisatChainType } from '@btc-vision/transaction';
import { AbstractRpcProvider, JSONRpcProvider } from 'opnet';
import type { WalletBase } from '../types';
import type { MyWalletInterface } from './interface';

interface MyWalletWindow extends Window {
  myWallet?: MyWalletInterface;
}

class MyWallet implements WalletBase {
  private walletBase: MyWalletWindow['myWallet'];
  private _isConnected: boolean = false;

  isInstalled(): boolean {
    if (typeof window === 'undefined') return false;
    this.walletBase = (window as unknown as MyWalletWindow).myWallet;
    return !!this.walletBase;
  }

  isConnected(): boolean {
    return !!this.walletBase && this._isConnected;
  }

  async canAutoConnect(): Promise<boolean> {
    const accounts = await this.walletBase?.getAccounts() || [];
    return accounts.length > 0;
  }

  getWalletInstance(): Unisat | null {
    return this._isConnected && this.walletBase || null;
  }

  async getProvider(): Promise<AbstractRpcProvider | null> {
    // Return appropriate provider based on network
    return new JSONRpcProvider('https://mainnet.opnet.org', networks.bitcoin);
  }

  async getSigner(): Promise<UnisatSigner | null> {
    // Return signer if supported
    return null;
  }

  async connect(): Promise<string[]> {
    if (!this.walletBase) throw new Error('Wallet not installed');
    const accounts = await this.walletBase.requestAccounts();
    this._isConnected = accounts.length > 0;
    return accounts;
  }

  async disconnect(): Promise<void> {
    await this.walletBase?.disconnect();
    this._isConnected = false;
  }

  async getPublicKey(): Promise<string | null> {
    return this.walletBase?.getPublicKey() || null;
  }

  async getNetwork(): Promise<UnisatChainType> {
    const chain = await this.walletBase?.getChain();
    return chain?.enum || UnisatChainType.BITCOIN_MAINNET;
  }

  // Implement event hooks
  setAccountsChangedHook(fn: (accounts: string[]) => void): void {
    this.walletBase?.on('accountsChanged', fn);
  }

  removeAccountsChangedHook(): void {
    // Remove listener
  }

  setDisconnectHook(fn: () => void): void {
    this.walletBase?.on('disconnect', fn);
  }

  removeDisconnectHook(): void {
    // Remove listener
  }

  setChainChangedHook(fn: (network: UnisatChainType) => void): void {
    this.walletBase?.on('chainChanged', (info) => fn(info.enum));
  }

  removeChainChangedHook(): void {
    // Remove listener
  }

  getChainId(): void {
    throw new Error('Method not implemented.');
  }

  // MLDSA methods (implement if supported)
  async getMLDSAPublicKey(): Promise<string | null> {
    return null;
  }

  async getHashedMLDSAKey(): Promise<string | null> {
    return null;
  }

  async signMLDSAMessage(message: string): Promise<MLDSASignature | null> {
    return null;
  }

  async verifyMLDSASignature(message: string, signature: MLDSASignature): Promise<boolean> {
    return false;
  }
}

export default MyWallet;

4. Add to Supported Wallets Enum

Update src/wallets/supported-wallets.ts:

export enum SupportedWallets {
  OP_WALLET = 'OP_WALLET',
  UNISAT = 'UNISAT',
  MY_WALLET = 'MY_WALLET',  // Add your wallet
}

5. Register the Wallet

Update src/wallets/index.ts:

import { WalletController } from './controller';
import MyWallet from './mywallet/controller';
import { logo as MyWalletLogo } from './mywallet/interface';
import { SupportedWallets } from './supported-wallets';

// ... existing registrations ...

WalletController.registerWallet({
  name: SupportedWallets.MY_WALLET,
  icon: MyWalletLogo,
  controller: new MyWallet(),
});

Error Handling

The library includes built-in error handling with internationalization support.

Connection Errors

Connection errors are displayed in the modal and can be accessed via error state:

// Errors are automatically displayed in the connection modal
// They auto-clear after 5 seconds

Common Error Messages

Error Cause Solution
"Wallet not found" Wallet extension not detected Install the wallet extension
"UNISAT is not installed" UniSat extension missing Install UniSat from Chrome Web Store
"OP_WALLET is not installed" OP_WALLET extension missing Install OP_WALLET from Chrome Web Store
"Failed to retrieve chain information" Network query failed Check wallet connection

Handling Errors in Code

const { connectToWallet } = useWalletConnect();

const handleConnect = async (wallet: SupportedWallets) => {
  try {
    await connectToWallet(wallet);
  } catch (error) {
    console.error('Connection failed:', error);
    // Handle error appropriately
  }
};

Migration Guide

Migrating from V1 to V2

Old version            -->      New version
{                               {
                                    allWallets
                                    openConnectModal
    connect                         connectToWallet
    disconnect                      disconnect
    walletType                      walletType
    walletWindowInstance            walletInstance
    account                         -
      - isConnected                 publicKey != null
      - signer                      signer
      - address                     address (Address.fromString(publicKey))
                                    publicKey (account publicKey)
                                    walletAddress (account address)
      - addressTyped
      - network                     network
      - provider                    provider
                                    connecting
} = useWallet()                 } = useWalletConnect()

Key Changes

  1. Hook rename: useWallet()useWalletConnect()
  2. Provider rename: WalletProviderWalletConnectProvider
  3. Flattened state: Account properties moved to top level
  4. New features: allWallets, openConnectModal, connecting, theme support
  5. MLDSA support: New quantum-resistant signature methods

Development

Clone and Install

git clone https://github.com/btc-vision/walletconnect.git
cd walletconnect
npm install

Build

# Build for Node.js
npm run build

# Build for browser
npm run browserBuild

# Build both
npm run setup

Development Mode

npm run watch

Linting

npm run lint

Check Circular Dependencies

npm run check:circular

Contributing

Contributions are welcome! Please read through the CONTRIBUTING.md file for guidelines on how to submit issues, feature requests, and pull requests.

License

This project is open source and available under the Apache-2.0 License.


For more information, visit docs.opnet.org or the OP_NET GitHub organization.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •