@turnkey/core
Version:
A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.
263 lines (259 loc) • 10.3 kB
JavaScript
;
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