npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ohmywallet/connect

v0.6.2

Published

OhMyWallet dApp integration library - Secure PassKey wallet connection via iframe

Readme

@ohmywallet/connect

The official library for integrating OhMyWallet into your dApp.

한국어

What is OhMyWallet?

OhMyWallet is a next-generation multi-chain smart wallet powered by PassKeys.

Supports EVM, Solana, and Bitcoin with a single PassKey.

Why OhMyWallet?

| Traditional Wallet Problems | OhMyWallet Solution | | --------------------------- | -------------------------------------- | | Memorize 12/24 seed words | Biometric auth (Face ID, fingerprint) | | Private key exposure risk | Isolated in hardware security chip | | Phishing attacks | Origin verification & iframe isolation | | New wallet per chain | Single multi-chain wallet |

Key Benefits

  • No Seed Phrase: Create and recover wallets with PassKey only
  • Hardware Security: Private keys never exposed to browser
  • Cross-Device: Same wallet on all devices via iCloud/Google sync
  • Multi-Chain: EVM, Solana, Bitcoin from one PassKey
  • WalletConnect: Connect to thousands of dApps
  • 15 Languages: Full i18n support (ko, en, zh-CN, zh-TW, ja, es, fr, de, pt, ru, ar, hi, id, vi, tr)

Installation

npm install @ohmywallet/connect
# or
pnpm add @ohmywallet/connect
# or
yarn add @ohmywallet/connect

Which Signer Should I Use?

OhMyWallet supports two signing methods. Choose explicitly based on your needs:

🔐 PassKey Signer (Hardware Security ★★★★★)

Recommended for: Chains with RIP-7212 support

Pros:

  • Hardware-secured: Private key never exposed (stored in device secure chip)
  • WebAuthn: Uses biometrics (Face ID, Touch ID, Windows Hello)
  • P-256 native: Direct signature verification on-chain
  • Maximum security: Best protection against key theft

Cons:

  • Limited chains: Only RIP-7212 compatible chains
  • Smart wallet required: Ethereum mainnet needs AA (Account Abstraction)

Supported Chains:

| Chain | Chain ID | Native RIP-7212 | Notes | | ------------- | -------- | --------------- | --------------------- | | zkSync Era | 324 | ✅ | Full support | | Polygon zkEVM | 1101 | ✅ | Full support | | Linea | 59144 | ✅ | Full support | | Scroll | 534352 | ✅ | Full support | | Ethereum* | 1 | ⚠️ | Requires smart wallet | | Arbitrum* | 42161 | ⚠️ | Requires smart wallet | | Optimism* | 10 | ⚠️ | Requires smart wallet |

*Can use PassKey via Account Abstraction (ERC-4337)


🔑 Derivation Signer (Universal ★★★☆☆)

Recommended for: All chains without RIP-7212, or multi-chain dApps

Pros:

  • Universal: Works on all chains (EVM, Solana, Bitcoin, etc.)
  • Multi-chain: Single PassKey → derive keys for any chain
  • No AA required: Works with EOAs (regular wallets)
  • All curves: secp256k1 (EVM/Bitcoin), ed25519 (Solana)

Cons:

  • ⚠️ Lower security: Private key exists in JavaScript memory (iframe-isolated)
  • ⚠️ Software-based: Not hardware-secured like PassKey

Supported Chains:

  • EVM: Ethereum, Arbitrum, Optimism, Base, BSC, Polygon, Avalanche, Fantom...
  • Non-EVM: Solana, Bitcoin, Cosmos, Near...
  • All chains: Any blockchain using secp256k1 or ed25519

Decision Guide

Your dApp uses...

┌─────────────────────────────────┐
│  zkSync Era, Polygon zkEVM,     │
│  Linea, Scroll                  │  →  ✅ Use PassKey (best security)
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Ethereum mainnet (EOA)         │
│  Arbitrum, Optimism, Base       │  →  ✅ Use Derivation
│  BSC, Polygon PoS               │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Solana, Bitcoin                │  →  ✅ Use Derivation (only option)
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Multi-chain support            │  →  ✅ Use Derivation (universal)
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Ethereum mainnet (AA wallet)   │  →  ✅ Use PassKey (if you implement AA)
└─────────────────────────────────┘

⚠️ Important: PassKey and Derivation generate different addresses. Choose one and stick with it for your dApp.


Quick Start

Option 1: PassKey Signer (Hardware Security)

import { IframeHost } from "@ohmywallet/connect";

// 1. Create wallet instance
const wallet = new IframeHost({
  iframeSrc: "https://vault.ohmywallet.xyz",
});

// 2. Connect with PassKey
const result = await wallet.connectWithSignerType({
  signerType: "passkey", // Explicit choice
  dappName: "My Awesome dApp",
  dappIcon: "https://my-dapp.com/icon.png",
});

// 3. Sign with active PassKey (P-256)
if (!result.activePasskey) {
  throw new Error("No active PassKey");
}

const sig = await wallet.signWithPasskey("0x1234...abcd", {
  keyId: result.activePasskey.keyId, // Use active PassKey
});

// 4. Cleanup
wallet.destroy();

Option 2: Derivation Signer (Universal)

import { IframeHost } from "@ohmywallet/connect";

// 1. Create wallet instance
const wallet = new IframeHost({
  iframeSrc: "https://vault.ohmywallet.xyz",
});

// 2. Connect with Derivation (connection only, no address info)
await wallet.connectWithSignerType({
  signerType: "derivation", // Explicit choice
});

// 3. Derive address (required step after connect)
const { address: primaryAddress } = await wallet.deriveAddress({
  keyIndex: 0,
  group: "evm",
  curve: "secp256k1",
});

// 4. Sign with derived address
const sig = await wallet.signWithDerivation("0x1234...abcd", {
  address: primaryAddress.address,
});

// 5. Cleanup
wallet.destroy();

API Reference

IframeHost

Main class for communicating with OhMyWallet from your dApp.

Constructor

const wallet = new IframeHost(config: IframeHostConfig);

| Option | Type | Required | Description | | ----------- | ----------------- | -------- | ---------------------------------------------------------------- | | iframeSrc | string | ✅ | OhMyWallet iframe URL | | timeout | number | - | Request timeout (default: 30000ms) | | sandbox | string | - | iframe sandbox attribute | | container | HTMLElement | - | Container for iframe insertion | | locale | SupportedLocale | - | iframe UI locale - 15 languages supported (default: auto-detect) | | origin | string | - | dApp origin (default: window.location.origin) |

Methods

connectWithSignerType(options): Promise<ConnectResult>

Connect using a signer type.

const passkey = await wallet.connectWithSignerType({
  signerType: "passkey",
  dappName: "My dApp",
});

const derivation = await wallet.connectWithSignerType({
  signerType: "derivation",
});
signWithPasskey(hash, options): Promise<PasskeySignResult>

P-256 signature using PassKey.

const sig = await wallet.signWithPasskey("0x1234...abcd", {
  keyId: passkey.passkeys[0].keyId,
});
signWithDerivation(hash, options): Promise<DerivationSignResult>

Signature using a derived key (secp256k1/ed25519).

Option 1: By address (recommended)

// First, derive the address
const { address } = await wallet.deriveAddress({
  keyIndex: 0,
  group: "evm",
  curve: "secp256k1",
});

// Then sign with the derived address
const sig = await wallet.signWithDerivation("0x1234...abcd", {
  address: address.address,
});

Option 2: By group + keyIndex

const sig = await wallet.signWithDerivation("0x1234...abcd", {
  group: "evm", // "evm" | "solana" | "bitcoin"
  keyIndex: 0,
});
deriveAddress(options): Promise<DeriveAddressResult>

Derive a new address without connecting.

const result = await wallet.deriveAddress({
  keyIndex: 1,
  curve: "secp256k1", // "secp256k1" | "ed25519"
  group: "evm", // "evm" | "solana" | "bitcoin"
});
// → { address: { address: "0x...", keyIndex: 1, curve: "secp256k1", group: "evm" } }

// For Bitcoin
const btcResult = await wallet.deriveAddress({
  keyIndex: 0,
  curve: "secp256k1",
  group: "bitcoin",
  bitcoinAddressType: "p2wpkh", // "p2wpkh" | "p2tr"
  bitcoinNetwork: "mainnet", // "mainnet" | "testnet4"
});
destroy()

Cleanup instance and release resources.

Properties

| Property | Type | Description | | -------------- | ----------------- | ---------------------------------------------------------------- | | currentState | IframeHostState | Current state (idle, loading, ready, error, destroyed) |

Events

// General events
wallet.onEvent("error", (error) => { ... });
wallet.onEvent("destroyed", () => { ... });

Framework Integration

React

import { useEffect, useState, useRef } from "react";
import { IframeHost } from "@ohmywallet/connect";
import type { Address } from "viem";

function useOhMyWallet() {
  const walletRef = useRef<IframeHost | null>(null);
  const [address, setAddress] = useState<Address | null>(null);
  const [isConnecting, setIsConnecting] = useState(false);

  useEffect(() => {
    walletRef.current = new IframeHost({
      iframeSrc: "https://vault.ohmywallet.xyz",
    });

    return () => walletRef.current?.destroy();
  }, []);

  const connect = async () => {
    if (!walletRef.current) return;
    setIsConnecting(true);
    try {
      // 1. Connect (connection only)
      await walletRef.current.connectWithSignerType({
        signerType: "derivation",
      });

      // 2. Derive address (required step)
      const { address: derived } = await walletRef.current.deriveAddress({
        keyIndex: 0,
        group: "evm",
        curve: "secp256k1",
      });

      setAddress(derived.address as Address);
    } finally {
      setIsConnecting(false);
    }
  };

  const sign = async (hash: `0x${string}`) => {
    if (!walletRef.current) throw new Error("Wallet not initialized");
    if (!address) throw new Error("No address selected");
    return walletRef.current.signWithDerivation(hash, { address });
  };

  return { address, isConnecting, connect, sign };
}

function WalletButton() {
  const { address, isConnecting, connect } = useOhMyWallet();

  if (address) {
    return (
      <span>
        {address.slice(0, 6)}...{address.slice(-4)}
      </span>
    );
  }

  return (
    <button onClick={connect} disabled={isConnecting}>
      {isConnecting ? "Connecting..." : "Connect Wallet"}
    </button>
  );
}

Vue 3

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { IframeHost } from "@ohmywallet/connect";

const wallet = ref<IframeHost | null>(null);
const address = ref<string | null>(null);
const isConnecting = ref(false);

onMounted(() => {
  wallet.value = new IframeHost({
    iframeSrc: "https://vault.ohmywallet.xyz",
  });
});

onUnmounted(() => {
  wallet.value?.destroy();
});

async function connect() {
  if (!wallet.value) return;
  isConnecting.value = true;
  try {
    // 1. Connect (connection only)
    await wallet.value.connectWithSignerType({
      signerType: "derivation",
    });

    // 2. Derive address (required step)
    const { address: derived } = await wallet.value.deriveAddress({
      keyIndex: 0,
      group: "evm",
      curve: "secp256k1",
    });

    address.value = derived.address;
  } finally {
    isConnecting.value = false;
  }
}
</script>

<template>
  <button @click="connect" :disabled="isConnecting">
    {{ address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "Connect Wallet" }}
  </button>
</template>

Vanilla JavaScript

<script type="module">
  import { IframeHost } from "https://esm.sh/@ohmywallet/connect";

  const wallet = new IframeHost({
    iframeSrc: "https://vault.ohmywallet.xyz",
  });

  document.getElementById("connect-btn").onclick = async () => {
    // 1. Connect (connection only)
    await wallet.connectWithSignerType({
      signerType: "derivation",
    });

    // 2. Derive address (required step)
    const { address: derived } = await wallet.deriveAddress({
      keyIndex: 0,
      group: "evm",
      curve: "secp256k1",
    });

    document.getElementById("address").textContent = derived.address;
  };

  document.getElementById("sign-btn").onclick = async () => {
    const hash = "0x" + "ab".repeat(32);
    const address = document.getElementById("address").textContent;
    if (!address || address === "-") return;
    const signature = await wallet.signWithDerivation(hash, { address });
    console.log("Signature:", signature);
  };
</script>

<button id="connect-btn">Connect Wallet</button>
<button id="sign-btn">Sign Message</button>
<p>Address: <span id="address">-</span></p>

Security

Iframe Isolation

OhMyWallet uses an iframe isolation architecture:

┌─────────────────────────────────┐
│  dApp (your-dapp.com)           │
│  ┌───────────────────────────┐  │
│  │ @ohmywallet/connect       │  │
│  │ (IframeHost)              │  │
│  └───────────┬───────────────┘  │
│              │ postMessage      │
│  ┌───────────▼───────────────┐  │
│  │ iframe                    │  │
│  │ (vault.ohmywallet.xyz)    │  │
│  │                           │  │
│  │  ┌─────────────────────┐  │  │
│  │  │ PassKey + Private   │  │  │
│  │  │ Key (never exposed) │  │  │
│  │  └─────────────────────┘  │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘
  • Key Isolation: Private keys exist only within iframe context
  • Origin Verification: Only authorized dApps can communicate
  • postMessage Communication: Only signatures transmitted (never private keys)

Recommended CSP

frame-src https://vault.ohmywallet.xyz;

Error Handling

import { IframeError } from "@ohmywallet/connect";

try {
  await wallet.connectWithSignerType({ signerType: "derivation" });
} catch (error) {
  if (error instanceof IframeError) {
    switch (error.code) {
      case "TIMEOUT":
        console.error("Request timed out");
        break;
      case "USER_CANCELLED":
        console.error("User cancelled");
        break;
      case "NOT_INITIALIZED":
        console.error("Wallet not initialized");
        break;
      default:
        console.error("Wallet error:", error.message);
    }
  }
}

Error Codes

| Code | Description | | ------------------------- | --------------------------------- | | NOT_INITIALIZED | Wallet not initialized | | ALREADY_INITIALIZED | Already initialized | | TIMEOUT | Request timed out | | DESTROYED | Instance destroyed | | SIGN_FAILED | Signature failed | | INVALID_MESSAGE | Invalid message format | | INVALID_ORIGIN | Unauthorized origin | | VALIDATION_FAILED | Payload validation failed | | CREDENTIAL_INACCESSIBLE | PassKey credential not accessible | | ALREADY_EXISTS | Wallet already exists | | USER_CANCELLED | User cancelled the operation | | UNKNOWN_KEY | PassKey keyId not found | | UNKNOWN_ADDRESS | Derivation address not found |

Advanced Usage

ZKsync Era Integration

When integrating with ZKsync Era, you need to properly structure EIP-712 transactions. Here's a complete example:

import { IframeHost } from "@ohmywallet/connect";
import { Contract, Interface, parseUnits, hexlify } from "ethers";
import { Provider, EIP712Signer, utils } from "zksync-ethers";

// 1. Connect wallet
const wallet = new IframeHost({
  iframeSrc: "https://vault.ohmywallet.xyz",
});

await wallet.connectWithSignerType({
  signerType: "derivation",
});

// Derive address (required step after connect)
const { address: derivedAddress } = await wallet.deriveAddress({
  keyIndex: 0,
  group: "evm",
  curve: "secp256k1",
});

const userAddress = derivedAddress.address;

// 2. Setup provider and contract
const provider = new Provider("https://sepolia.era.zksync.dev");
const contract = new Contract(contractAddress, contractABI, provider);

// 3. Encode function data
const iface = new Interface(contractABI);
const data = iface.encodeFunctionData("transfer", [toAddress, parseUnits("1", 18)]);

// 4. Build transaction
const [chain, feeData, nonce] = await Promise.all([
  provider.getNetwork(),
  provider.getFeeData(),
  provider.getTransactionCount(userAddress),
]);

const tx = {
  type: utils.EIP712_TX_TYPE,
  chainId: chain.chainId,
  from: userAddress,
  to: contractAddress,
  data,
  value: 0n,
  maxFeePerGas: feeData.maxFeePerGas ?? 1n,
  maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? feeData.maxFeePerGas ?? 1n,
  nonce,
  gasLimit: 1_000_000n, // Estimate first if needed
  customData: {
    gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
    customSignature: "0x", // Will be filled after signing
  },
};

// 5. Generate EIP-712 digest and sign
const digest = EIP712Signer.getSignedDigest(tx);
const signResult = await wallet.signWithDerivation(hexlify(digest), {
  address: userAddress,
});

// 6. ⚠️ IMPORTANT: Add signature to transaction
tx.customData.customSignature = signResult.signature;

// 7. Serialize and broadcast
const serialized = utils.serializeEip712(tx);
const response = await provider.broadcastTransaction(serialized);
await response.wait();

console.log("Transaction hash:", response.hash);

Important Notes for ZKsync Era

  1. Custom Signature Field

    // ❌ Wrong: Signature in second parameter
    const serialized = utils.serializeEip712(tx, signature);
    
    // ✅ Correct: Signature in tx.customData
    tx.customData.customSignature = signature;
    const serialized = utils.serializeEip712(tx);
  2. EIP-712 Digest

    • Use EIP712Signer.getSignedDigest(tx) to generate the correct digest
    • Sign the digest (not the transaction directly)
  3. Gas Estimation

    const estimate = await provider.estimateGas({
      ...tx,
      customData: {
        gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
      },
    });
    tx.gasLimit = estimate;
  4. Paymaster Support (Optional)

    const paymasterParams = utils.getPaymasterParams(paymasterAddress, {
      type: "ApprovalBased",
      token: tokenAddress,
      minimalAllowance: gasLimit * maxFeePerGas,
      innerInput: new Uint8Array(),
    });
    
    tx.customData.paymasterParams = paymasterParams;

Content Security Policy (CSP)

If your dApp uses strict CSP headers, ensure they don't block React event handlers or Next.js features:

Recommended CSP for Next.js:

// next.config.js
async headers() {
  return [
    {
      source: "/:path*",
      headers: [
        {
          key: "Content-Security-Policy",
          value: [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline'", // Required for React
            "frame-src https://vault.ohmywallet.xyz",
            "connect-src 'self' https://*.zksync.dev", // Add RPC endpoints
          ].join("; "),
        },
      ],
    },
  ];
}

Common CSP Issues:

  • script-src 'self' only → Blocks React event handlers
  • Missing frame-src → Blocks OhMyWallet iframe
  • Too strict in development → Use conditional CSP:
if (process.env.NODE_ENV === "development") {
  return []; // No CSP in development
}

TypeScript

This package is written in TypeScript and provides complete type definitions.

import type {
  IframeHostConfig,
  IframeHostState,
  ConnectResult,
  SignResult,
  IframeErrorCode,
} from "@ohmywallet/connect";

Type Guards

Use type guards to safely narrow union types:

import {
  isPasskeyResult,
  isDerivationResult,
  isPasskeySignResult,
  isDerivationSignResult,
} from "@ohmywallet/connect";

// Connect result
const result = await wallet.connectWithSignerType({ signerType: "passkey" });

if (isPasskeyResult(result)) {
  // TypeScript knows result is PasskeyConnectResult
  console.log(result.passkeys);
  console.log(result.activePasskey?.keyId);
}

if (isDerivationResult(result)) {
  // TypeScript knows result is DerivationConnectResult
  // Note: addresses are obtained via deriveAddress(), not from connect result
  console.log(result.signerType); // "derivation"
}

// Sign result
const sig = await wallet.signWithPasskey(hash, { keyId });

if (isPasskeySignResult(sig)) {
  // TypeScript knows sig is PasskeySignResult
  console.log(sig.signature.r, sig.signature.s);
  console.log(sig.authenticatorData);
}

if (isDerivationSignResult(sig)) {
  // TypeScript knows sig is DerivationSignResult
  console.log(sig.signature); // Hex string
}

Reference Helpers

Check chain compatibility (for reference only, not for automatic selection):

import { supportsRIP7212, RIP7212_NATIVE_CHAINS } from "@ohmywallet/connect";

// Check if chain natively supports RIP-7212
if (supportsRIP7212(324)) {
  console.log("zkSync Era supports PassKey natively");
}

// List of chains with native RIP-7212 support
console.log(RIP7212_NATIVE_CHAINS); // [324, 1101, 59144, 534352]

// ⚠️ Important: Do NOT use for automatic signerType selection
// ❌ BAD: const signerType = supportsRIP7212(chainId) ? "passkey" : "derivation";
// ✅ GOOD: Always choose signerType explicitly

Browser Support

  • Chrome 67+
  • Firefox 60+
  • Safari 14+
  • Edge 79+

Requires WebAuthn (PassKey) support.

License

MIT

Links