UNPKG

@turnkey/core

Version:

A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.

263 lines (259 loc) 10.3 kB
'use strict'; var SignClient = require('@walletconnect/sign-client'); /** * WalletConnectClient is a low-level wrapper around the WalletConnect SignClient. * * - Used internally by `WalletConnectWallet` to manage connections and sessions. * - Handles pairing, approval, session tracking, RPC requests, and disconnects. * - Exposes a minimal API for lifecycle control; higher-level logic lives in `WalletConnectWallet`. */ class WalletConnectClient { constructor() { // tracks the pending approval callback returned from `connect()` this.pendingApproval = null; // these are subscribers we forward from the underlying WalletConnect SignClient: // // - sessionUpdateHandlers: fired on 'session_update' when session namespaces // (accounts/chains/permissions) change // // - sessionEventHandlers: fired on 'session_event' for ephemeral wallet events // like { event: { name: 'chainChanged' | 'accountsChanged', data }, chainId, topic } // // - sessionDeleteHandlers: fired on 'session_delete' when the session is terminated this.sessionUpdateHandlers = []; this.sessionEventHandlers = []; this.sessionDeleteHandlers = []; this.pairingExpireHandlers = []; } /** * Registers a callback for WalletConnect `session_update`. * * - Triggered when the wallet updates the session namespaces * (e.g., accounts/chains/permissions). Use this to refresh providers. * - Returns an unsubscribe function to remove the listener. * * @param fn - Callback invoked when a `session_update` occurs. * @returns A function that unsubscribes this listener. */ onSessionUpdate(fn) { this.sessionUpdateHandlers.push(fn); return () => { this.sessionUpdateHandlers = this.sessionUpdateHandlers.filter((h) => h !== fn); }; } /** * Registers a callback for WalletConnect `session_event`. * * - Emits ephemeral wallet events such as `chainChanged` or `accountsChanged`. * - The raw event payload is forwarded so callers can inspect `{ event, chainId, topic }`. * - Returns an unsubscribe function to remove the listener. * * @param fn - Callback receiving the raw session event args. * @returns A function that unsubscribes this listener. */ onSessionEvent(fn) { this.sessionEventHandlers.push(fn); return () => { this.sessionEventHandlers = this.sessionEventHandlers.filter((h) => h !== fn); }; } /** * Registers a callback for WalletConnect `session_delete`. * * - Fired from the underlying SignClient when a session is terminated. * - Useful for clearing UI state or internal session data after a disconnect. * * @param fn - Callback to invoke when the session is deleted. */ onSessionDelete(fn) { this.sessionDeleteHandlers.push(fn); } /** * Registers a callback for WalletConnect `pairing_expire`. * * - Fired from the underlying SignClient when a session proposal expires. * - Useful for re-initiating the pairing process, and refreshing the pairing URI. * * @param fn - Callback to invoke when the pairing expires. */ onPairingExpire(fn) { this.pairingExpireHandlers.push(fn); } /** * Initializes the WalletConnect SignClient with your project credentials. * * - Must be called before `pair()`, `approve()`, or `request()`. * - Configures app metadata and an optional custom relay server. * * @param opts.projectId - WalletConnect project ID. * @param opts.appMetadata - Metadata about your app (name, URL, icons). * @param opts.relayUrl - (Optional) custom relay server URL. * @returns A promise that resolves once the client is initialized. */ async init(opts) { this.client = await SignClient.init({ projectId: opts.projectId, metadata: opts.appMetadata, ...(opts.relayUrl ? { relayUrl: opts.relayUrl } : {}), }); // fan out WalletConnect SignClient events to our subscribers this.client.on("session_delete", () => { this.sessionDeleteHandlers.forEach((h) => h()); }); this.client.on("session_update", () => { this.sessionUpdateHandlers.forEach((h) => h()); }); this.client.on("session_event", (args) => { this.sessionEventHandlers.forEach((h) => h(args)); }); this.client.core.pairing.events.on("pairing_expire", () => { this.pairingExpireHandlers.forEach((h) => h()); }); } /** * Initiates a pairing request and returns a URI to be scanned or deep-linked. * * - Requires `init()` to have been called. * - Must be followed by `approve()` after the wallet approves. * - Throws if a pairing is already in progress. * * @param namespaces - Optional namespaces requesting capabilities. * @returns A WalletConnect URI for the wallet to connect with. * @throws {Error} If a pairing is already in progress or no URI is returned. */ async pair(namespaces) { if (this.pendingApproval) { throw new Error("WalletConnect: Pairing already in progress"); } const { uri, approval } = await this.client.connect({ optionalNamespaces: namespaces, }); if (!uri) { throw new Error("WalletConnect: no URI returned"); } this.pendingApproval = approval; return uri; } /** * Completes the pairing approval process after the wallet approves the request. * * - Requires `init()` and a pending pairing started via `pair()`. * * @returns A promise that resolves to the established session. * @throws {Error} If called before `pair()` or if approval fails. */ async approve() { if (!this.pendingApproval) { throw new Error("WalletConnect: call pair() before approve()"); } try { const session = await this.pendingApproval(); return session; } catch (error) { // we sanitize common errors to be more user-friendly if (error.message?.includes("Proposal expired")) { throw new Error("WalletConnect: The connection request has expired. Please scan the QR code again."); } if (error.message?.includes("User rejected")) { throw new Error("WalletConnect: The connection request was rejected by the user."); } throw new Error(`WalletConnect: Approval failed: ${error.message}`); } finally { // we clear the pending state regardless of outcome this.pendingApproval = null; } } /** * Retrieves the most recent active WalletConnect session. * * @returns The most recent session, or `null` if none are active. */ getSession() { // we return null if the client hasn't been initialized yet if (!this.client?.session) { return null; } const sessions = this.client.session.getAll(); return sessions.length ? sessions[sessions.length - 1] : null; } /** * Sends a JSON-RPC request over the active WalletConnect session. * * - Requires `init()` and an active session. * * @param chainId - Target chain ID (e.g. `eip155:1`). * @param method - RPC method name. * @param params - Parameters for the RPC method. * @returns A promise that resolves with the RPC response. * @throws {Error} If no active session exists. */ async request(chainId, method, params) { const session = this.getSession(); if (!session) { throw new Error("WalletConnect: no active session"); } return this.client.request({ topic: session.topic, chainId, request: { method, params }, }); } /** * Disconnects the active session, if one exists. * * - Sends a disconnect signal to the wallet. * - Does nothing if no session is currently active. * * @returns A promise that resolves once disconnection is complete. */ async disconnect() { const session = this.getSession(); if (!session) return; await this.client.disconnect({ topic: session.topic, reason: { code: 6000, message: "User disconnected" }, }); } /** * Cancels all in-progress WalletConnect pairings, if any. * * - Iterates through the core pairing registry and disconnects every active pairing * - Always clears the pending approval callback so subsequent `pair()` calls * can be made * - Safe to call even if no pairings exist. it simply resets internal state * * @returns A promise that resolves once cancellation is complete. */ async cancelPairing() { try { const pairings = this.client.core.pairing.getPairings(); // disconnect all active pairings // technically there should only be one active pairing at a tim const disconnectPromises = pairings.map(async (pairing) => { try { await this.client.core.pairing.disconnect({ topic: pairing.topic, }); } catch (err) { if (err.message?.includes("Expired") || err.message?.includes("Not found") || err.message?.includes("No matching key")) { // already expired/disconnected, so we ignore return; } console.warn("failed to disconnect pairing:", err); } }); await Promise.allSettled(disconnectPromises); } finally { this.pendingApproval = null; } } } exports.WalletConnectClient = WalletConnectClient; //# sourceMappingURL=client.js.map