@meshconnect/uwc-core
v1.1.2
Published
Core functionality for Universal Wallet Connector
Downloads
8,855
Maintainers
Keywords
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
- Install
- Quick start
- Core concepts
- Configuration
- Creating the instance
- Reading state
- Events
- Connecting a wallet
- Disconnecting
- Switching networks
- Signing messages
- Sending transactions
- Wallet capabilities
- Cancelling in-flight operations
- Error handling
- Testing
- Recipes
- API reference
Install
npm install @meshconnect/uwc-core @meshconnect/uwc-types @meshconnect/uwc-constantsThe 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 === connectorgetInstance() 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> | nullPrefer 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 payloadLegacy 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
- Resolves the wallet + provider + target network.
- Fires
connecting. - Starts the underlying wallet flow (extension RPC call, WC pairing, TON JS
bridge). For QR-based flows, polls for the pairing URI and fires
connectionUrias soon as it appears. - On success, updates the session and fires
connected+sessionChanged. - On failure, resets the active connector and rethrows.
errorfires withoperation: '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/onLognon-blocking. They are called synchronously on the wallet-operation path — a terminal event fires between the wallet returning a result and that result reaching yourawait, so a slow handler adds latency to connect / sign / sendTransaction. Queue-backed sinks (DatadogaddAction, Segmenttrack) 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 errorA 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-compaterrorevent. The duplicateerrorrecord carrieshandoffCovered: true, so failure metrics are: terminals +errorwhere NOThandoffCovered(the uncovered ones are pre-wallet validation failures, which have no terminal). Counting terminals and allerrorrecords 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), andpageHidden/pageVisibleDuringHandofffire on each visibility toggle while a handoff is pending. If you forwardonEventstraight to a metered backend (Datadog RUM, etc.), sample or filter bynameon 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(): voidConstructors
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
@meshconnect/uwc-react— React bindings (hooks + provider)@meshconnect/uwc-types— all request/response/session types@meshconnect/uwc-constants— ready-madeNetworkobjectsapps/vanilla-example— complete vanilla-TypeScript reference app using this package directly
