Skip to main content

Overview

This example demonstrates how to use Hermis with vanilla JavaScript - no framework required! The guide shows examples for both web3.js (legacy) and @solana/kit (modern) architectures.

Architecture Guide

Web3.js (Legacy)

  • Mature and widely supported
  • Compatible with all existing Solana tools
  • Well-documented ecosystem
  • Larger bundle size

@solana/kit (Modern)

  • Type-safe instruction building
  • Better tree-shaking for smaller bundles
  • Modern API design
  • Future-proof architecture
Good news: Both architectures work seamlessly with WalletAdapterManager! You can even use Kit signers with wallet adapters through createKitSignersFromAdapter.

Project Setup

mkdir solana-wallet-app
cd solana-wallet-app
npm init -y

# For web3.js architecture
npm install @hermis/solana-headless-adapter-base @hermis/errors
npm install @solana/wallet-adapter-phantom @solana/wallet-adapter-solflare @solana/web3.js

# For Kit architecture (optional)
npm install @solana/web-sdk @solana-program/system @solana/kit

HTML Structure

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Solana Wallet App</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>Solana Wallet Integration</h1>

    <!-- Wallet Selection -->
    <div id="wallet-selector" class="wallet-selector"></div>

    <!-- Wallet Info (hidden by default) -->
    <div id="wallet-info" class="wallet-info" style="display: none;">
      <h2>Connected Wallet</h2>
      <p><strong>Wallet:</strong> <span id="wallet-name"></span></p>
      <p><strong>Address:</strong> <span id="wallet-address"></span></p>
      <p><strong>Balance:</strong> <span id="wallet-balance">Loading...</span> SOL</p>
      <button id="disconnect-btn" class="button button-secondary">Disconnect</button>
    </div>
  </div>

  <script type="module" src="app.js"></script>
</body>
</html>

Styling

style.css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.container {
  background: white;
  padding: 40px;
  border-radius: 20px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  max-width: 600px;
  width: 100%;
}

h1 {
  color: #333;
  margin-bottom: 30px;
  text-align: center;
}

.wallet-selector {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.wallet-option {
  display: flex;
  align-items: center;
  padding: 15px 20px;
  border: 2px solid #e0e0e0;
  border-radius: 10px;
  background: white;
  cursor: pointer;
  transition: all 0.3s;
}

.wallet-option:hover {
  border-color: #667eea;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}

.wallet-option.installed {
  border-color: #14F195;
  background: #f0fdf4;
}

.wallet-info {
  text-align: center;
}

.wallet-info p {
  margin: 15px 0;
  color: #666;
}

.wallet-info strong {
  color: #333;
}

.button {
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.3s;
  margin-top: 20px;
}

.button-secondary {
  background: #ff4757;
  color: white;
}

.button-secondary:hover {
  background: #ff3838;
}

#wallet-address {
  font-family: 'Courier New', monospace;
  font-size: 14px;
}

Wallet Connection

import { WalletAdapterManager, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base';
import { Connection, clusterApiUrl, LAMPORTS_PER_SOL } from '@solana/web3.js';

// Global state
let manager;
let connection;

// Initialize wallet manager and connection
function initWalletManager() {
  connection = new Connection(clusterApiUrl('devnet'));

  // Auto-detect wallet-standard compatible wallets
  const wallets = []; // Add custom wallet adapters here if needed
  const adapters = getStandardWalletAdapters(wallets);

  manager = new WalletAdapterManager(adapters);
  setupEventListeners();
  renderWalletOptions();
}

// Setup wallet event listeners
function setupEventListeners() {
  manager.on('connect', async (publicKey) => {
    console.log('Connected:', publicKey.toBase58());
    showWalletInfo(publicKey.toBase58());
    await updateBalance(publicKey);
  });

  manager.on('disconnect', () => {
    console.log('Disconnected');
    hideWalletInfo();
  });

  manager.on('error', (error) => {
    console.error('Error:', error);
    alert(`Wallet error: ${error.message}`);
  });

  document.getElementById('disconnect-btn').onclick = () => {
    manager.disconnect();
  };
}

// Render available wallet options
function renderWalletOptions() {
  const container = document.getElementById('wallet-selector');
  const adapters = manager.getAdapters();

  adapters.forEach(adapter => {
    const button = document.createElement('button');
    button.className = 'wallet-option';
    button.textContent = adapter.name;

    if (adapter.readyState === 'Installed') {
      button.classList.add('installed');
      const badge = document.createElement('span');
      badge.textContent = ' ✓ Installed';
      badge.style.color = '#14F195';
      badge.style.marginLeft = '10px';
      button.appendChild(badge);
    }

    button.onclick = async () => {
      manager.selectAdapter(adapter.name);
      try {
        await manager.connect();
      } catch (error) {
        console.error('Connection failed:', error);
      }
    };

    container.appendChild(button);
  });
}

// Update wallet balance
async function updateBalance(publicKey) {
  try {
    const balance = await connection.getBalance(publicKey);
    const solBalance = balance / LAMPORTS_PER_SOL;
    document.getElementById('wallet-balance').textContent = solBalance.toFixed(4);
  } catch (error) {
    console.error('Failed to get balance:', error);
    document.getElementById('wallet-balance').textContent = 'Error';
  }
}

// Show wallet info UI
function showWalletInfo(address) {
  document.getElementById('wallet-name').textContent = manager.getSelectedAdapter()?.name || 'Unknown';
  document.getElementById('wallet-address').textContent = address;
  document.getElementById('wallet-info').style.display = 'block';
  document.getElementById('wallet-selector').style.display = 'none';
}

// Hide wallet info UI
function hideWalletInfo() {
  document.getElementById('wallet-info').style.display = 'none';
  document.getElementById('wallet-selector').style.display = 'flex';
}

// Initialize on load
initWalletManager();

Send Transaction

import { Transaction, SystemProgram, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';

// Send SOL transaction
async function sendSOL(recipientAddress, amountInSOL) {
  const publicKey = manager.getAdapter()?.publicKey;
  if (!publicKey) {
    throw new Error('Wallet not connected');
  }

  try {
    const transaction = new Transaction();
    transaction.add(
      SystemProgram.transfer({
        fromPubkey: publicKey,
        toPubkey: new PublicKey(recipientAddress),
        lamports: amountInSOL * LAMPORTS_PER_SOL
      })
    );

    const { blockhash } = await connection.getLatestBlockhash();
    transaction.recentBlockhash = blockhash;
    transaction.feePayer = publicKey;

    const signature = await manager.signAndSendTransaction(connection, transaction);

    if (!signature) {
      throw new Error('Transaction failed');
    }

    console.log('✅ Transaction signature:', signature);

    // Wait for confirmation
    await connection.confirmTransaction(signature, 'confirmed');
    console.log('✅ Transaction confirmed!');

    return signature;
  } catch (error) {
    console.error('❌ Transaction error:', error);
    throw error;
  }
}

// Wire up send button
document.getElementById('send-btn').onclick = async () => {
  const recipient = document.getElementById('recipient').value;
  const amount = parseFloat(document.getElementById('amount').value);
  const statusEl = document.getElementById('tx-status');

  if (!recipient || !amount) {
    statusEl.textContent = 'Please enter recipient and amount';
    return;
  }

  try {
    statusEl.textContent = 'Sending transaction...';
    const signature = await sendSOL(recipient, amount);
    statusEl.textContent = `Success! Signature: ${signature.substring(0, 20)}...`;
  } catch (error) {
    statusEl.textContent = `Error: ${error.message}`;
  }
};
HTML for transaction UI:
<!-- Add to wallet-info div -->
<div class="transaction-form">
  <h3>Send SOL</h3>
  <input type="text" id="recipient" placeholder="Recipient Address" />
  <input type="number" id="amount" placeholder="Amount (SOL)" step="0.01" min="0" />
  <button id="send-btn" class="button button-primary">Send Transaction</button>
  <p id="tx-status"></p>
</div>

Sign Message

// Sign message with wallet
async function signMessage(message) {
  const publicKey = manager.getAdapter()?.publicKey;
  if (!publicKey) {
    throw new Error('Wallet not connected');
  }

  try {
    // Encode message to Uint8Array
    const encodedMessage = new TextEncoder().encode(message);

    // Sign using manager
    const signature = await manager.signMessage(encodedMessage);

    if (!signature) {
      throw new Error('Message signing failed');
    }

    console.log('✅ Signature:', signature);
    return signature;
  } catch (error) {
    console.error('❌ Signing error:', error);
    throw error;
  }
}

// Wire up sign button
document.getElementById('sign-btn').onclick = async () => {
  const message = document.getElementById('message-input').value;
  const outputEl = document.getElementById('signature-output');

  if (!message) {
    outputEl.textContent = 'Please enter a message';
    return;
  }

  try {
    outputEl.textContent = 'Signing message...';
    const signature = await signMessage(message);
    outputEl.textContent = `Signature: ${Array.from(signature).slice(0, 10).join(', ')}...`;
  } catch (error) {
    outputEl.textContent = `Error: ${error.message}`;
  }
};
HTML for message signing:
<!-- Add to wallet-info div -->
<div class="message-form">
  <h3>Sign Message</h3>
  <textarea id="message-input" placeholder="Enter message to sign"></textarea>
  <button id="sign-btn" class="button button-primary">Sign Message</button>
  <p id="signature-output"></p>
</div>

Build Setup

package.json
{
  "name": "solana-wallet-app",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "devDependencies": {
    "vite": "^5.0.0"
  }
}

Running the Example

npm run dev
Open http://localhost:5173 in your browser!

Features Demonstrated

Web3.js Architecture

Wallet connection with WalletAdapterManager
SOL balance fetching
Transaction sending (SOL transfers)
Message signing
Event-driven wallet state
Error handling

Kit Architecture

Kit signer creation from adapters
Kit transaction building with pipe pattern
Kit message signing with signBytes
Type-safe instruction building
Functional composition with pipe
Modern RPC interface

Both Architectures

Same WalletAdapterManager for connection
Seamless bridge via createKitSignersFromAdapter
Multi-wallet support
Event-driven architecture