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

@meshconnect/uwc-core

v1.1.2

Published

Core functionality for Universal Wallet Connector

Downloads

8,855

Readme

@meshconnect/uwc-core

Framework-agnostic wallet connection manager for Web3 applications. Single API for injected wallets (MetaMask, Phantom, Tonkeeper extension, …), WalletConnect (EVM + Solana mobile wallets), and TON Connect (TON wallets via the JS Bridge).

If you are building a React app, reach for @meshconnect/uwc-react instead — it wraps this package with hooks and a context provider. This document is for everyone else: vanilla JS apps, Svelte, Vue, Solid, or any non-React runtime.


Table of contents

  1. Install
  2. Quick start
  3. Core concepts
  4. Configuration
  5. Creating the instance
  6. Reading state
  7. Events
  8. Connecting a wallet
  9. Disconnecting
  10. Switching networks
  11. Signing messages
  12. Sending transactions
  13. Wallet capabilities
  14. Cancelling in-flight operations
  15. Error handling
  16. Testing
  17. Recipes
  18. API reference

Install

npm install @meshconnect/uwc-core @meshconnect/uwc-types @meshconnect/uwc-constants

The types package provides the WalletMetadata, Network, TransactionRequest, … definitions used throughout the API. The constants package ships ready-to-use Network objects (mainnetNetwork, baseNetwork, solanaNetwork, …) so you don't have to hand-roll chain configs.


Quick start

import { UniversalWalletConnector } from '@meshconnect/uwc-core'
import {
  mainnetNetwork,
  baseNetwork,
  solanaNetwork
} from '@meshconnect/uwc-constants'
import type { WalletMetadata } from '@meshconnect/uwc-types'

const metamask: WalletMetadata = {
  id: 'metamask',
  name: 'MetaMask',
  metadata: { icon: 'https://example.com/metamask.png' },
  extensionInjectedProvider: {
    supportedNetworkIds: ['eip155:1', 'eip155:8453'],
    namespaceMetaData: {
      eip155: {
        eip155Name: 'metamask',
        injectedId: 'isMetamask',
        supportsAddingNetworks: true,
        requiresUserApprovalOnNetworkSwitch: false
      }
    },
    requiresUserApprovalOnNamespaceSwitch: false
  },
  walletConnectProvider: {
    supportedNetworkIds: ['eip155:1', 'eip155:8453'],
    deeplinks: {
      universal: 'https://metamask.app.link',
      native: 'metamask://'
    }
  }
}

const connector = UniversalWalletConnector.getInstance({
  networks: [mainnetNetwork, baseNetwork, solanaNetwork],
  wallets: [metamask],
  walletConnectConfig: {
    projectId: 'YOUR_WC_PROJECT_ID',
    metadata: {
      name: 'My dApp',
      description: 'Does Web3 things',
      url: 'https://my-dapp.xyz',
      icons: ['https://my-dapp.xyz/icon.png']
    }
  }
})

// React to state changes
connector.on('connected', ({ session }) => {
  console.log('connected as', session.activeAddress)
})
connector.on('connectionUri', ({ uri }) => {
  console.log('show QR code for:', uri)
})
connector.on('error', ({ error, operation }) => {
  console.error(`${operation} failed:`, error.type, error.message)
})

// Connect
await connector.connect('injected', 'metamask', 'eip155:1')

Core concepts

ConnectionMode

Three mutually exclusive ways a user can connect:

| Mode | Description | Typical wallets | | --------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------- | | injected | Browser-extension or in-app injected provider (EIP-6963, Solana Wallet Standard, Tron, TON JS Bridge) | MetaMask, Phantom, Tonkeeper extension | | walletConnect | WalletConnect v2 relay + QR code / deeplink | Any WC-compatible mobile wallet | | tonConnect | TON Connect JS Bridge + QR code / universal link | Tonkeeper, OKX TON Wallet |

A wallet can support multiple modes. isConnectionModeAvailable(mode, walletId) tells you which ones are actually viable in the current runtime.

Session

One opaque object describes "what's connected right now":

interface Session {
  connectionMode: ConnectionMode | null
  activeWallet: WalletMetadata | null
  activeNetwork: Network | null
  activeAddress: string | null
  publicKey: string | null
  availableNetworks: Network[] // networks the current wallet supports
  availableAddresses: AvailableAddress[] // one per chain the wallet exposed
  activeWalletCapabilities: Record<string, EVMCapabilities> | null
  activeNetworkWalletCapabilities: EVMCapabilities | null
}

getSession() always returns the current snapshot. When any field changes, a sessionChanged event fires.

Observer / event model

The connector is an event emitter. The state-change events are:

| Event | When it fires | | --------------------- | -------------------------------------------------------- | | ready | Initial wallet detection has completed | | walletsDetected | Detection completed, payload is the enriched wallet list | | connecting | connect() started | | connectionUri | A WC/TonConnect pairing URI is now available | | connected | connect() finished successfully | | disconnected | disconnect() finished (session cleared) | | sessionChanged | Any session field changed | | networkSwitching | Network switch started or ended | | networkSwitched | Network switch finished successfully | | capabilitiesUpdated | getWalletCapabilities() refreshed data | | error | A user-initiated op threw | | change | Catch-all — fires after every other state-change event |

Plus a set of telemetry-only events used for logging and wallet-handoff visibility (see Logging & observability). These deliberately do not cascade to change (so they never trigger re-renders): log, awaitingWallet, walletSucceeded, walletRejected, walletFailed, walletTimedOut, walletDiscoveryFailed, bridgeError, relayError, walletAccountChanged, walletChainChanged, walletDisconnected, walletSessionExpired, lifecycleTelemetryUnavailable, pageHiddenDuringHandoff, pageVisibleDuringHandoff — the full list is the TELEMETRY_ONLY_EVENTS export in events.ts.

Use on(eventName, listener) for targeted updates. Every state-change event also cascades to change, so the legacy subscribe(listener) pattern still works.

AbortSignal

Every async operation accepts an optional { signal }. If the signal aborts, the operation rejects with the signal's reason — a DOMException named AbortError by default, or whatever you passed to controller.abort(reason) — and no session mutations happen after the abort, even if the wallet prompt has already returned. Wallet prompts themselves cannot usually be cancelled — the signal protects your app from acting on stale results.


Configuration

Networks

A Network describes a chain. @meshconnect/uwc-constants ships the common ones; you only need to hand-write one if you're adding a chain the library doesn't know about. The only required contract is: every networkId your wallets claim to support must have a matching Network in this array — otherwise the connector will refuse to pick that network.

Wallets

Each WalletMetadata entry declares which connection modes the wallet can use and the per-mode provider config. Provide only the providers that apply:

const phantom: WalletMetadata = {
  id: 'phantom',
  name: 'Phantom',
  metadata: { icon: '…' },

  // Extension / in-browser injection (EIP-6963 + Solana Wallet Standard)
  extensionInjectedProvider: {
    supportedNetworkIds: ['eip155:1', 'solana:5eykt4UsFv8P…'],
    namespaceMetaData: {
      eip155: {
        eip155Name: 'phantom',
        injectedId: 'isPhantom',
        supportsAddingNetworks: true,
        requiresUserApprovalOnNetworkSwitch: false
      },
      solana: { walletStandardName: 'Phantom', injectedId: 'isPhantom' }
    },
    requiresUserApprovalOnNamespaceSwitch: false
  }

  // Omit walletConnectProvider / tonConnectProvider if unsupported
}

usingIntegratedBrowser: true switches the injected path to use integratedBrowserInjectedProvider instead of extensionInjectedProvider — use it when your app runs inside a wallet's built-in browser (e.g. Trust Wallet DApp browser).

WalletConnect / TON Connect

Only required if at least one wallet advertises that mode:

walletConnectConfig: {
  projectId: 'YOUR_WC_PROJECT_ID',
  metadata: { name, description, url, icons }
}

tonConnectConfig: {
  manifestUrl: 'https://my-dapp.xyz/tonconnect-manifest.json'
}

Omit a config and the corresponding connector simply isn't instantiated — isConnectionModeAvailable('walletConnect', …) will return false.


Creating the instance

The library enforces a single instance per page. There are three ways to create it; pick one and stick with it.

Recommended — getInstance()

const connector = UniversalWalletConnector.getInstance({
  networks,
  wallets,
  walletConnectConfig // optional
})

// Anywhere else in your app, call it again without config:
const same = UniversalWalletConnector.getInstance()
// → same === connector

getInstance() builds the instance on the first call and returns the cached one afterwards. Calling it without config before any instance exists throws.

Config-object constructor

const connector = new UniversalWalletConnector({
  networks,
  wallets,
  walletConnectConfig
})

The first new call is auto-registered as the singleton, so UniversalWalletConnector.getInstance() later returns the same reference.

Positional constructor (legacy)

const connector = new UniversalWalletConnector(
  networks,
  wallets,
  /* usingIntegratedBrowser */ false,
  walletConnectConfig,
  tonConnectConfig
)

Constructing a second instance logs an error through the configured logger — it's almost always a mistake. For tests and full-logout flows, call UniversalWalletConnector.resetInstance() to clear the cached reference.


Reading state

All reads are synchronous and cheap. Call them any time.

connector.getSession() // current Session
connector.getState() // { session } — parity with older API
connector.isReady() // true once detection completed
connector.getWallets() // wallets with `installed` flags set
connector.getNetworks() // the configured networks array
connector.isConnectionModeAvailable('walletConnect', 'metamask')
connector.getConnectionURI() // WC/TonConnect pairing URI, if any
connector.getNetworkSwitchLoadingState()
// { isLoading, isWaitingForUserApproval }
connector.getActiveWalletCapabilities() // Record<NetworkId, EVMCapabilities> | null

Prefer subscribing to events over polling — see the next section.


Events

on(event, listener)

const unsubscribe = connector.on('sessionChanged', ({ session }) => {
  render(session)
})

// Clean up when no longer needed
unsubscribe()

once(event, listener)

Fires at most once, then auto-removes:

connector.once('ready', () => {
  console.log('wallet detection done')
})

off(event, listener)

Manual removal if you didn't keep the on() return value:

function handler({ uri }) { … }
connector.on('connectionUri', handler)
// …later
connector.off('connectionUri', handler)

Event payloads

import type { UWCEventMap } from '@meshconnect/uwc-core'

// Narrowed per event:
connector.on('connecting', ({ connectionMode, walletId }) => {})
connector.on('connected', ({ session }) => {})
connector.on('disconnected', () => {})
connector.on('connectionUri', ({ uri, connectionMode }) => {})
connector.on(
  'networkSwitching',
  ({ isLoading, isWaitingForUserApproval }) => {}
)
connector.on('networkSwitched', ({ network }) => {})
connector.on('sessionChanged', ({ session }) => {})
connector.on('capabilitiesUpdated', ({ capabilities }) => {})
connector.on('walletsDetected', ({ wallets }) => {})
connector.on('ready', () => {})
connector.on('error', ({ error, operation }) => {})
connector.on('change', () => {}) // catch-all; no payload

Legacy subscribe()

Still supported; subscribes to the catch-all change event:

const unsubscribe = connector.subscribe(() => {
  // anything changed — re-read getSession() etc.
})

Prefer the typed events for new code.


Connecting a wallet

try {
  await connector.connect('injected', 'metamask')
  // or pick a specific chain:
  await connector.connect('injected', 'metamask', 'eip155:8453')
} catch (error) {
  // see "Error handling" below
}

Displaying a WalletConnect / TonConnect QR

The pairing URI appears asynchronously. Listen for connectionUri and display the QR as soon as it fires:

connector.on('connectionUri', async ({ uri, connectionMode }) => {
  if (connectionMode === 'walletConnect') {
    await renderQRCode(uri)
  }
})

await connector.connect('walletConnect', 'metamask')
// connect() resolves only once the user has scanned + approved.
// The promise can reject if the user cancels or the proposal expires.

WalletConnect proposal-expiry retry

WalletConnect pairings time out if the user doesn't scan within ~5 minutes. The error's type is 'expired'. Retry with a fresh call:

async function connectWithRetry(walletId: string, networkId?: NetworkId) {
  for (;;) {
    try {
      await connector.connect('walletConnect', walletId, networkId)
      return
    } catch (error) {
      if ((error as WalletError).type === 'expired') continue
      throw error
    }
  }
}

What connect() does internally

  1. Resolves the wallet + provider + target network.
  2. Fires connecting.
  3. Starts the underlying wallet flow (extension RPC call, WC pairing, TON JS bridge). For QR-based flows, polls for the pairing URI and fires connectionUri as soon as it appears.
  4. On success, updates the session and fires connected + sessionChanged.
  5. On failure, resets the active connector and rethrows. error fires with operation: 'connect'.

Disconnecting

await connector.disconnect()

Clears the session and fires disconnected + sessionChanged. If the underlying connector throws (e.g. wallet refuses, network error), the error is re-emitted on the typed error event and the promise rejects — wrap the call in a try/catch in app-level "sign out" flows so the UI keeps moving.


Switching networks

await connector.switchNetwork('eip155:8453') // Base

// Track loading for UI
connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {
  if (isWaitingForUserApproval) showBanner('Approve in your wallet…')
  else if (isLoading) showBanner('Switching network…')
  else hideBanner()
})

Prerequisites: an active session, the target networkId must be in the connector's configured networks, and the active wallet must list it in supportedNetworkIds. Otherwise switchNetwork throws before prompting the wallet.

Switching across namespaces (e.g. EVM → Solana) works when the wallet's requiresUserApprovalOnNamespaceSwitch metadata is set correctly — the user may see an extra prompt.


Signing messages

import type { SignatureType } from '@meshconnect/uwc-types'

const signature: SignatureType = await connector.signMessage('Hello, world')

switch (signature.type) {
  case 'standard': // EVM, Solana
    console.log(signature.signature)
    break
  case 'tron':
    console.log(signature.txID, signature.signature)
    break
  case 'tvm': // TON Connect signData
    console.log(signature.signature, signature.domain, signature.timestamp)
    // MUST verify domain + timestamp before trusting this signature.
    break
}

Throws if no wallet is connected, the connector doesn't support signing, or the user rejects the prompt (error.type === 'rejected').


Sending transactions

sendTransaction takes a namespace-specific request object and returns a TransactionResult (chain-dependent hash / signature).

EVM — native transfer (ETH, MATIC, …)

import type { EVMNativeTransferRequest } from '@meshconnect/uwc-types'

const req: EVMNativeTransferRequest = {
  from: session.activeAddress!,
  to: '0xRecipient…',
  amount: 1_000_000_000_000_000_000n, // 1 ETH in wei
  gasConfig: { gasLimit: 21_000 }
}
const hash = await connector.sendTransaction(req)

EVM — contract call (e.g. ERC-20 transfer)

import type { EVMContractCallRequest } from '@meshconnect/uwc-types'
import { erc20ABI } from './abi/erc20ABI'

const req: EVMContractCallRequest = {
  contractAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // USDC on Base
  abi: erc20ABI,
  functionName: 'transfer',
  args: [recipient, amountInSmallestUnit],
  from: session.activeAddress!
}
const hash = await connector.sendTransaction(req)

Solana — native transfer

import type { SolanaNativeTransferRequest } from '@meshconnect/uwc-types'

// Fetch a recent blockhash first (RPC call you own)
const blockhash = await fetchLatestBlockhash()

const req: SolanaNativeTransferRequest = {
  from: session.activeAddress!,
  to: recipient,
  amount: 1_000_000_000n, // 1 SOL in lamports
  blockhash
}
const signature = await connector.sendTransaction(req)

TON Jetton (USDT on TON, etc.)

Use the helpers in @meshconnect/uwc-ton-connector to build the BOC payload, then pass the resulting TonNativeTransferRequest to sendTransaction. See the project root README for details.


Wallet capabilities

For EVM wallets supporting EIP-5792 (wallet_getCapabilities), the connector can fetch capability metadata — atomic batching, paymaster support, etc.

await connector.getWalletCapabilities(
  session.activeAddress!,
  session.activeNetwork ?? undefined
)

const caps = connector.getActiveWalletCapabilities()
if (caps?.['eip155:8453']?.atomic?.status === 'supported') {
  // wallet can atomically batch on Base
}

capabilitiesUpdated fires with the new map when the call succeeds. Capability data is written into the session, so you can also read it from getSession() as activeWalletCapabilities / activeNetworkWalletCapabilities.


Cancelling in-flight operations

Every async operation accepts { signal }:

const controller = new AbortController()

connectButton.addEventListener('click', () => {
  connector.connect('walletConnect', 'metamask', undefined, {
    signal: controller.signal
  })
})

cancelButton.addEventListener('click', () => controller.abort())

When the signal aborts, the in-flight promise rejects with the signal's reason (an AbortError unless you aborted with a custom one). Session state is not mutated after the abort, even if the wallet prompt later returns a result. This is especially useful in component lifecycles:

// Single-page app: abort on route change
const controller = new AbortController()
router.once('beforeLeave', () => controller.abort())
await connector.signMessage('Log in', { signal: controller.signal })

Note: wallet extensions and mobile wallets don't expose a way to dismiss an open prompt from the dApp side — the abort protects your state, but the user may still see the prompt until they dismiss it.


Error handling

All operations throw either a plain Error (validation / setup issues) or a WalletConnectorError (wallet-side failures). The connector also emits a typed error event for every throw, so you can centralise logging.

The WalletError shape

interface WalletError {
  type: 'unknown' | 'rejected' | 'expired'
  message: string
}
  • rejected — user dismissed the prompt.
  • expired — WalletConnect pairing proposal timed out.
  • unknown — everything else (network errors, bad wallet state, …).

Per-call handling

try {
  await connector.connect('injected', 'metamask')
} catch (error) {
  const walletError = error as WalletError
  if (walletError.type === 'rejected') {
    toast('You cancelled the connection')
  } else {
    toast(`Connect failed: ${walletError.message}`)
  }
}

Centralised logging via events

import type { UWCOperation } from '@meshconnect/uwc-core'

connector.on(
  'error',
  ({ error, operation }: { error: WalletError; operation: UWCOperation }) => {
    analytics.track('wallet.error', {
      operation,
      type: error.type,
      message: error.message
    })
  }
)

operation is one of: 'connect' | 'disconnect' | 'switchNetwork' | 'signMessage' | 'signTypedData' | 'sendTransaction' | 'signSolanaTransaction' | 'getWalletCapabilities' | 'initialize'.

AbortError

Aborted operations reject with the signal's reason: a DOMException whose .name === 'AbortError' by default, or your own value when you called controller.abort(customReason). Either way they are not routed through the error event — UWC consults the signal itself (not just the error name), so even a custom-reason abort is treated as a cancellation, not a wallet failure.

try {
  await connector.connect(..., { signal })
} catch (error) {
  if ((error as { name?: string }).name === 'AbortError') {
    // silent: user navigated away
    return
  }
  throw error
}

Logging & observability

UWC ships no third-party telemetry SDK. Instead it exposes two opt-in seams you wire to whatever stack you run (Datadog, Amplitude, Segment, a plain console). With neither configured, there is zero external egress — the security-first default for a sign-only wallet layer.

Logging

A pluggable, level-gated logger backs UWC's internal logging. Configure it via UWCConfig:

const connector = new UniversalWalletConnector({
  networks,
  wallets,
  logLevel: 'info', // console threshold; default 'warn'
  logger: myAppLogger // optional: redirect output into your own logger
})

logger (any object with debug/info/warn/error) receives the raw (message, ...args); omit it and a console sink is used with an ISO/level prefix.

Tune the threshold at runtime — useful when triaging in a wallet's in-app browser where you can't redeploy:

connector.setLogLevel('debug') // programmatic
window.UWC_DEBUG = true // or from the console: surfaces everything
window.UWC_DEBUG = 'info' // or a specific level (only ever lowers it)

Observer (telemetry)

Implement UWCObserver to receive a normalized, PII-safe event stream plus all log lines. The vendor code lives in your app, never in UWC:

import type { UWCObserver } from '@meshconnect/uwc-core'

const observer: UWCObserver = {
  onEvent: e => datadogRum.addAction(`uwc.${e.name}`, e), // or amplitude.track / …
  onLog: (level, message, args) =>
    myAppLogger[level](`[uwc] ${message}`, ...args)
}

const connector = new UniversalWalletConnector({ networks, wallets, observer })

Keep onEvent / onLog non-blocking. They are called synchronously on the wallet-operation path — a terminal event fires between the wallet returning a result and that result reaching your await, so a slow handler adds latency to connect / sign / sendTransaction. Queue-backed sinks (Datadog addAction, Segment track) are already non-blocking; for anything heavy, defer it yourself (onEvent: e => queueMicrotask(() => …)).

Every onEvent record is a UWCTelemetryEvent — only routing facets (operation, connectionMode, namespace, chainId, walletId, durationMs, outcome…) plus a sdkSessionId correlation id. By default sdkSessionId is a generated per-instance id; pass correlationId (a string or getter) to align it to your own session key (e.g. link_sess_id) so UWC events join your existing Datadog/RUM telemetry without a separate mapping:

new UniversalWalletConnector({
  ...config,
  observer,
  correlationId: () => myLinkSession.id // resolved per-event; getter may be lazy/dynamic
})

Structured fields never carry a signing payload or wallet address — the shape has no slot for them. The two free-form surfaces (error.message and onLog args) additionally pass through a key + content scrubber that redacts sensitive keys, EVM hex blobs (addresses / signatures / keys), secret URL params, bearer tokens, and Solana base58 / TON (EQ…/UQ…) addresses. This last pass is best-effort and biased toward over-redaction (some non-secret base58-looking ids/hashes get redacted by design). One gap remains: BARE (non-0x) hex — a raw hex string is far more often a legitimate id/hash than a secret, so only 0x-prefixed hex is redacted. Structured fields never carry addresses regardless.

Wallet-handoff funnel

The moment UWC hands control to a wallet (extension popup, WC deeplink, TON sheet) it emits awaitingWallet; when the wallet answers it emits exactly one terminal — all sharing a handoffId and carrying durationMs:

awaitingWallet ─┬─ walletSucceeded   success
                ├─ walletRejected    user said no (4001 / rejected)
                └─ walletFailed      any other error

A non-cancelling walletTimedOut marker also fires if a non-TON handoff is still pending past walletResponseTimeoutMs (default 60s, 0 disables). It is purely observational — it never rejects, aborts, or mutates state, and is cleared the instant the op settles, so a fast op never false-fires it. This turns the previously-blind connect/sign/approve window into a per-(chain × wallet × flow) timed funnel.

A failed or rejected op emits both a handoff terminal (walletFailed / walletRejected) and the back-compat error event. The duplicate error record carries handoffCovered: true, so failure metrics are: terminals + error where NOT handoffCovered (the uncovered ones are pre-wallet validation failures, which have no terminal). Counting terminals and all error records double-counts each wallet failure. One documented edge: a wallet that rejects with a bare string/number (not an object) can't be marked — a WeakSet can't hold primitives — so that rare failure double-counts. EIP-1193 requires object errors, and wrapping the thrown value to make it markable would mutate what the caller receives.

Volume. A single connect can emit several records (awaitingWallet, a terminal, plus any focus / provider-lifecycle events), and pageHidden/pageVisibleDuringHandoff fire on each visibility toggle while a handoff is pending. If you forward onEvent straight to a metered backend (Datadog RUM, etc.), sample or filter by name on your side — UWC emits every record and does not sample for you. Provider-lifecycle events are additionally capped at 100 per attachment (MAX_LIFECYCLE_EVENTS_PER_ATTACHMENT) so a storming provider cannot drive unbounded egress.

Teardown

Call dispose() on full logout to release observer subscriptions, tear down any live provider-lifecycle listeners on a connected session, and clear all listeners:

connector.dispose()

Testing

The shared singleton can leak state between tests. Reset it in beforeEach:

import { UniversalWalletConnector } from '@meshconnect/uwc-core'

beforeEach(() => {
  UniversalWalletConnector.resetInstance()
})

resetInstance() also clears the instance-count and duplicate-warning flags, so each test starts from a clean slate.

When unit-testing consumer code, mock the whole module:

vi.mock('@meshconnect/uwc-core', () => ({
  UniversalWalletConnector: class {
    static getInstance = () => new this()
    getSession = () => ({
      /* fixture */
    })
    on = () => () => {}
    once = () => () => {}
    off = () => {}
    subscribe = () => () => {}
    connect = vi.fn()
    disconnect = vi.fn()
    signMessage = vi.fn()
    sendTransaction = vi.fn()
    // …whichever methods your code exercises
  }
}))

Recipes

Minimal vanilla app — connect, display address, disconnect

const connector = UniversalWalletConnector.getInstance({
  networks: [mainnetNetwork],
  wallets: [metamask]
})

connector.on('sessionChanged', ({ session }) => {
  document.querySelector('#addr')!.textContent =
    session.activeAddress ?? 'not connected'
})

document
  .querySelector('#connect')!
  .addEventListener('click', () => connector.connect('injected', 'metamask'))
document
  .querySelector('#disconnect')!
  .addEventListener('click', () => connector.disconnect())

Show QR and auto-close modal on pair

connector.on('connectionUri', async ({ uri, connectionMode }) => {
  if (connectionMode !== 'walletConnect') return
  qrModal.show(await QRCode.toDataURL(uri))
})

connector.on('connected', () => qrModal.hide())
connector.on('error', ({ operation }) => {
  if (operation === 'connect') qrModal.hide()
})

await connector.connect('walletConnect', 'metamask')

Network switcher with per-state UI

connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {
  switchBtn.disabled = isLoading
  switchBtn.textContent = isWaitingForUserApproval
    ? 'Approve in wallet…'
    : isLoading
      ? 'Switching…'
      : 'Switch network'
})

switchBtn.addEventListener('click', () =>
  connector.switchNetwork('eip155:8453')
)

Abort a pending sign when the user navigates away

const controller = new AbortController()
window.addEventListener('beforeunload', () => controller.abort())

try {
  const sig = await connector.signMessage('Sign in', {
    signal: controller.signal
  })
  submitLogin(sig)
} catch (error) {
  if ((error as { name?: string }).name === 'AbortError') return
  throw error
}

API reference

Types

export interface UWCConfig {
  networks: Network[]
  // Required: the constructor throws on an empty/missing wallet list.
  wallets: WalletMetadata[]
  usingIntegratedBrowser?: boolean
  walletConnectConfig?: WalletConnectConfig
  tonConnectConfig?: TonConnectConfig
  tronConnectorConfig?: TronConnectorConfig
  // Observability (all optional; defaults preserve current behaviour)
  observer?: UWCObserver // vendor-agnostic telemetry sink; default = no egress
  logger?: Logger // redirect internal logging; default = console
  logLevel?: LogLevel // console threshold; default 'warn'
  walletResponseTimeoutMs?: number // walletTimedOut deadline; default 60_000, 0 disables
  correlationId?: string | (() => string) // align sdkSessionId to your session key
}

export interface OperationOptions {
  signal?: AbortSignal
}

export type UWCEventName = keyof UWCEventMap
export type UWCOperation =
  | 'connect'
  | 'disconnect'
  | 'switchNetwork'
  | 'signMessage'
  | 'signTypedData'
  | 'sendTransaction'
  | 'signSolanaTransaction'
  | 'getWalletCapabilities'
  | 'initialize'

Static methods

UniversalWalletConnector.getInstance(config?: UWCConfig): UniversalWalletConnector
UniversalWalletConnector.resetInstance(): void

Constructors

new UniversalWalletConnector(config: UWCConfig)
new UniversalWalletConnector(
  networks: Network[],
  wallets: WalletMetadata[],
  usingIntegratedBrowser?: boolean,
  walletConnectConfig?: WalletConnectConfig,
  tonConnectConfig?: TonConnectConfig,
  tronConnectorConfig?: TronConnectorConfig
)

Instance methods

// State
getSession(): Session
getState(): { session: Session }
isReady(): boolean
getWallets(): WalletMetadata[]
getNetworks(): Network[]
getConnectionURI(): string | undefined
getNetworkSwitchLoadingState(): {
  isLoading: boolean
  isWaitingForUserApproval: boolean
}
getActiveWalletCapabilities(): Record<string, EVMCapabilities> | null
isConnectionModeAvailable(mode: ConnectionMode, walletId: string): boolean

// Observability
setLogLevel(level: LogLevel): void   // tune the console threshold at runtime
dispose(): void                      // release observer subs + clear listeners

// Events
on<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): () => void
once<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): () => void
off<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): void
subscribe(listener: () => void): () => void  // legacy; maps to `change`

// Async ops — all accept an optional OperationOptions argument
connect(
  mode: ConnectionMode,
  walletId: string,
  networkId?: NetworkId,
  options?: OperationOptions
): Promise<void>

disconnect(options?: OperationOptions): Promise<void>

switchNetwork(
  networkId: NetworkId,
  options?: OperationOptions
): Promise<void>

signMessage(
  message: string,
  options?: OperationOptions
): Promise<SignatureType>

sendTransaction(
  request: TransactionRequest,
  options?: OperationOptions
): Promise<TransactionResult>

getWalletCapabilities(
  address: string,
  activeNetwork?: Network,
  options?: OperationOptions
): Promise<void>

Further reading