UNPKG

@cosmos-kit/walletconnect

Version:

cosmos-kit wallet connector using walletconnect

569 lines (568 loc) 21.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WCClient = void 0; const core_1 = require("@cosmos-kit/core"); const sign_client_1 = __importDefault(require("@walletconnect/sign-client")); const utils_1 = require("./utils"); const EXPLORER_API = 'https://explorer-api.walletconnect.com'; class WCClient { walletInfo; signClient; wcCloudInfo; // info from WalletConnect Cloud Explorer actions; qrUrl; appUrl; pairings = []; sessions = []; emitter; logger; options; relayUrl; env; requiredNamespaces; _defaultSignOptions = { preferNoSetFee: false, preferNoSetMemo: true, disableBalanceCheck: true, }; constructor(walletInfo) { if (!walletInfo.walletconnect) { throw new Error(`'walletconnect' info for wallet ${walletInfo.prettyName} is not provided in wallet registry.`); } this.walletInfo = walletInfo; this.qrUrl = { state: core_1.State.Init }; this.appUrl = { state: core_1.State.Init }; this.requiredNamespaces = walletInfo.walletconnect.requiredNamespaces; } get defaultSignOptions() { return this._defaultSignOptions; } setDefaultSignOptions(options) { this._defaultSignOptions = options; } get isMobile() { return this.env?.device === 'mobile'; } // walletconnect wallet name get wcName() { return this.walletInfo.walletconnect.name; } // wallet defined bytes encoding get wcEncoding() { return this.walletInfo.walletconnect.encoding || 'hex'; } // walletconnect wallet project id get wcProjectId() { return this.walletInfo.walletconnect.projectId; } // walletconnect wallet mobile link get wcMobile() { return this.walletInfo.walletconnect.mobile; } get accounts() { const accounts = []; this.sessions.forEach((s) => { Object.entries(s.namespaces).forEach(([, nsValue]) => { nsValue.accounts.forEach((account) => { const [namespace, chainId, address] = account.split(':'); accounts.push({ namespace, chainId, address, }); }); }); }); return accounts; } deleteSession(topic) { const chainIds = []; this.sessions = this.sessions.filter((s) => { if (s.topic === topic) { s.namespaces.cosmos.accounts.forEach((account) => { const [, chainId] = account.split(':'); chainIds.push(chainId); }); return false; } else { return true; } }); this.emitter?.emit('reset', chainIds); this.logger?.debug('[WALLET EVENT] Emit `reset`'); } subscribeToEvents() { if (typeof this.signClient === 'undefined') { throw new Error('WalletConnect is not initialized'); } this.signClient.on('session_ping', (args) => { this.logger?.debug('EVENT', 'session_ping', args); }); this.signClient.on('session_event', async (args) => { this.logger?.debug('EVENT', 'session_event', args); // const { // topic, // params: { event, chainId }, // } = args; // if (this.session?.topic != topic) return; // if (event.name === 'accountsChanged') { // await this.connect( // this.accounts.map(([, chainId]) => chainId), // false // ); // } }); this.signClient.on('session_update', ({ topic, params }) => { this.logger?.debug('EVENT', 'session_update', { topic, params }); // if (this.session?.topic != topic) return; // const { namespaces } = params; // const _session = this.signClient.session.get(topic); // this.session = { ..._session, namespaces }; }); this.signClient.on('session_delete', (args) => { this.logger?.debug('EVENT', 'session_delete', args); this.deleteSession(args.topic); }); this.signClient.on('session_expire', (args) => { this.logger?.debug('EVENT', 'session_expire', args); this.deleteSession(args.topic); }); this.signClient.on('session_proposal', (args) => { this.logger?.debug('EVENT', 'session_proposal', args); }); this.signClient.on('session_request', (args) => { this.logger?.debug('EVENT', 'session_request', args); }); this.signClient.on('proposal_expire', (args) => { this.logger?.debug('EVENT', 'proposal_expire', args); }); } async deleteInactivePairings() { if (typeof this.signClient === 'undefined') { throw new Error('WalletConnect is not initialized'); } for (const pairing of this.signClient.pairing.getAll({ active: false })) { await this.signClient.pairing.delete(pairing.topic, { code: 7001, message: 'Clear inactive pairings.', }); this.logger?.debug('Delete inactive pairing:', pairing.topic); } } async deleteAllPairings() { if (typeof this.signClient === 'undefined') { throw new Error('WalletConnect is not initialized'); } for (const pairing of this.signClient.pairing.getAll()) { await this.signClient.pairing.delete(pairing.topic, { code: 7001, message: 'Clear pairings.', }); this.logger?.debug('Delete pairing:', pairing.topic); } } restorePairings() { if (typeof this.signClient === 'undefined') { throw new Error('WalletConnect is not initialized'); } this.pairings = this.signClient.pairing .getAll({ active: true }) .filter((p) => p.peerMetadata?.name === this.wcName && p.expiry * 1000 > Date.now() + 1000); this.logger?.debug('RESTORED PAIRINGS: ', this.pairings); } get pairing() { return this.pairings[0]; } restoreSessions() { if (typeof this.signClient === 'undefined') { throw new Error('WalletConnect is not initialized'); } this.sessions = this.signClient.session .getAll() .filter((s) => s.peer.metadata.name === this.wcName && s.expiry * 1000 > Date.now() + 1000); this.logger?.debug('RESTORED SESSIONS: ', this.sessions); } async getSession(namespace, chainId) { if (typeof this.signClient === 'undefined') { await this.init(); } return this.sessions.find((s) => s.namespaces[namespace]?.accounts?.find((account) => account.startsWith(`${namespace}:${chainId}`))); } get walletName() { return this.walletInfo.name; } get dappProjectId() { return this.options?.signClient.projectId; } setActions(actions) { this.actions = actions; } setQRState(state) { this.qrUrl.state = state; this.actions?.qrUrl?.state(state); } setQRError(e) { this.setQRState(core_1.State.Error); this.qrUrl.message = typeof e === 'string' ? e : e?.message; this.actions?.qrUrl?.message?.(this.qrUrl.message); if (typeof e !== 'string' && e?.stack) { this.logger?.error(e.stack); } } async init() { await this.initSignClient(); if (this.isMobile) { await this.initAppUrl(); } } async initSignClient() { if (this.signClient && this.relayUrl === this.options?.signClient.relayUrl) { return; } this.signClient = await sign_client_1.default.init(this.options?.signClient); this.relayUrl = this.options?.signClient.relayUrl; this.logger?.debug('CREATED CLIENT: ', this.signClient); this.logger?.debug('relayerRegion ', this.options?.signClient.relayUrl); this.subscribeToEvents(); this.restorePairings(); this.restoreSessions(); } async initWCCloudInfo() { const fetcUrl = `${EXPLORER_API}/v3/wallets?projectId=${this.dappProjectId}&sdks=sign_v2&search=${this.wcName}`; const fetched = await (await fetch(fetcUrl)).json(); this.wcCloudInfo = fetched.listings[this.walletInfo.walletconnect.projectId]; this.logger?.debug('WalletConnect Info:', this.wcCloudInfo); } async initAppUrl() { this.appUrl.state = core_1.State.Pending; if (!this.wcCloudInfo) await this.initWCCloudInfo(); const native = this.wcCloudInfo.mobile.native || this.wcMobile?.native; const universal = this.wcCloudInfo.mobile.universal || this.wcMobile?.universal; this.appUrl.data = { native, universal }; this.appUrl.state = core_1.State.Done; } get nativeUrl() { const native = this.appUrl.data?.native; if (typeof native === 'string' || typeof native === 'undefined') { return native; } else { const { android, ios, macos, windows } = native; switch (this.env?.os) { case 'android': return android; case 'ios': return ios; case 'macos': return macos; case 'windows': return windows; default: throw new Error(`Unknown os: ${this.env?.os}.`); } } } get universalUrl() { return this.appUrl.data?.universal; } get redirectHref() { return this.nativeUrl || this.universalUrl; } get redirectHrefWithWCUri() { let href; if (this.nativeUrl) { href = (this.walletInfo.walletconnect.formatNativeUrl || utils_1.CoreUtil.formatNativeUrl)(this.nativeUrl, this.qrUrl.data, this.env?.os, this.walletName); } else if (this.universalUrl) { href = (this.walletInfo.walletconnect.formatUniversalUrl || utils_1.CoreUtil.formatUniversalUrl)(this.universalUrl, this.qrUrl.data, this.walletName); } return href; } get displayQRCode() { if (this.pairing || this.redirect) { return false; } else { return true; } } get redirect() { return Boolean(this.isMobile && (this.nativeUrl || this.universalUrl)); } openApp(withWCUri = true) { const href = withWCUri ? this.redirectHrefWithWCUri : this.redirectHref; if (href) { this.logger?.debug('Redirecting:', href); utils_1.CoreUtil.openHref(href); } else { this.logger?.error('No redirecting href.'); } } async connect(chainIds, options) { if (typeof this.signClient === 'undefined') { await this.init(); // throw new Error('WalletConnect is not initialized'); } if (this.qrUrl.state !== 'Init') { this.setQRState(core_1.State.Init); } const chainIdsWithNS = typeof chainIds === 'string' ? [`cosmos:${chainIds}`] : chainIds.map((chainId) => `cosmos:${chainId}`); this.restorePairings(); const pairing = this.pairing; this.logger?.debug('Restored active pairing topic is:', pairing?.topic); if (this.displayQRCode) this.setQRState(core_1.State.Pending); const requiredNamespaces = { cosmos: { methods: [ 'cosmos_getAccounts', 'cosmos_signAmino', 'cosmos_signDirect', ...(this.requiredNamespaces?.methods ?? []), ], chains: chainIdsWithNS, events: [ 'chainChanged', 'accountsChanged', ...(this.requiredNamespaces?.events ?? []), ], }, }; let connectResp; try { this.logger?.debug('Connecting chains:', chainIdsWithNS); connectResp = await this.signClient.connect({ pairingTopic: pairing?.topic, requiredNamespaces, ...options, }); // https://github.com/hyperweb-io/projects-issues/issues/349 // Commented out because of the issue above. // if (typeof connectResp.uri === 'undefined') { // throw new Error('Failed to generate WalletConnect URI!'); // } this.qrUrl.data = connectResp.uri; this.logger?.debug('Using QR URI:', connectResp.uri); if (this.displayQRCode) this.setQRState(core_1.State.Done); } catch (error) { this.logger?.error('Client connect error: ', error); if (this.displayQRCode) this.setQRError(error); return; } if (this.redirect) this.openApp(); try { const session = await connectResp.approval(); this.logger?.debug('Established session:', session); this.sessions.push(session); this.restorePairings(); } catch (error) { this.logger?.error('Session approval error: ', error); await this.deleteInactivePairings(); if (!error) { if (this.displayQRCode) this.setQRError(core_1.ExpiredError); throw new Error('Proposal Expired'); } else if (error.code == 5001) { throw core_1.RejectedError; } else { throw error; } } finally { if (!pairing && this.qrUrl.message !== core_1.ExpiredError.message) { this.setQRState(core_1.State.Init); } } } async disconnect(options) { if (typeof this.signClient === 'undefined') { await this.init(); // throw new Error('WalletConnect is not initialized'); } if (options?.walletconnect?.removeAllPairings === true) { await this.deleteAllPairings(); } if (this.sessions.length === 0) { return; } for (const session of this.sessions) { try { this.logger?.debug('Delete session:', session); const { getSdkError } = await Promise.resolve().then(() => __importStar(require('@walletconnect/utils'))); await this.signClient.disconnect({ topic: session.topic, reason: getSdkError('USER_DISCONNECTED'), }); } catch (error) { this.logger?.error(`SignClient.disconnect session ${session.topic} failed:`, error); } } this.sessions = []; this.emitter?.emit('sync_disconnect'); this.logger?.debug('[WALLET EVENT] Emit `sync_disconnect`'); } async getSimpleAccount(chainId) { const account = this.accounts.find(({ chainId: id }) => id === chainId); if (!account) { throw new Error(`Chain ${chainId} is not connected yet, please check the session approval namespaces`); } return account; } getOfflineSignerAmino(chainId) { return { getAccounts: async () => [await this.getAccount(chainId)], signAmino: (signerAddress, signDoc) => this.signAmino(chainId, signerAddress, signDoc), }; } getOfflineSignerDirect(chainId) { return { getAccounts: async () => [await this.getAccount(chainId)], signDirect: (signerAddress, signDoc) => this.signDirect(chainId, signerAddress, signDoc), }; } async getOfflineSigner(chainId, preferredSignType) { if (preferredSignType === 'amino' && this.getOfflineSignerAmino) { return this.getOfflineSignerAmino(chainId); } if (preferredSignType === 'direct' && this.getOfflineSignerDirect) { return this.getOfflineSignerDirect(chainId); } return this.getOfflineSignerAmino ? this.getOfflineSignerAmino?.(chainId) : this.getOfflineSignerDirect(chainId); } async _getAccount(chainId) { const session = await this.getSession('cosmos', chainId); if (!session) { throw new Error(`Session for ${chainId} not established yet.`); } const resp = await this.signClient.request({ topic: session.topic, chainId: `cosmos:${chainId}`, request: { method: 'cosmos_getAccounts', params: {}, }, }); this.logger?.debug(`Response of cosmos_getAccounts`, resp); return resp; } async getAccount(chainId) { const { address, algo, pubkey } = (await this._getAccount(chainId))[0]; return { address, algo: algo, pubkey: new Uint8Array(Buffer.from(pubkey, this.wcEncoding)), }; } async _signAmino(chainId, signer, signDoc, signOptions) { const session = await this.getSession('cosmos', chainId); if (!session) { throw new Error(`Session for ${chainId} not established yet.`); } if (this.redirect) this.openApp(); const resp = await this.signClient.request({ topic: session.topic, chainId: `cosmos:${chainId}`, request: { method: 'cosmos_signAmino', params: { signerAddress: signer, signDoc, }, }, }); this.logger?.debug(`Response of cosmos_signAmino`, resp); return resp; } async signAmino(chainId, signer, signDoc, signOptions) { const result = (await this._signAmino(chainId, signer, signDoc, signOptions || this.defaultSignOptions)); return result; } async _signDirect(chainId, signer, signDoc, signOptions) { const session = await this.getSession('cosmos', chainId); if (!session) { throw new Error(`Session for ${chainId} not established yet.`); } const signDocValue = { signerAddress: signer, signDoc: { chainId: signDoc.chainId, bodyBytes: Buffer.from(signDoc.bodyBytes).toString(this.wcEncoding), authInfoBytes: Buffer.from(signDoc.authInfoBytes).toString(this.wcEncoding), accountNumber: signDoc.accountNumber.toString(), }, }; if (this.redirect) this.openApp(); const resp = await this.signClient.request({ topic: session.topic, chainId: `cosmos:${chainId}`, request: { method: 'cosmos_signDirect', params: signDocValue, }, }); this.logger?.debug(`Response of cosmos_signDirect`, resp); return resp; } async signDirect(chainId, signer, signDoc, signOptions) { const { signed, signature } = (await this._signDirect(chainId, signer, signDoc, signOptions || this.defaultSignOptions)); return { signed: { chainId: signed.chainId, accountNumber: BigInt(signed.accountNumber), authInfoBytes: new Uint8Array(Buffer.from(signed.authInfoBytes, this.wcEncoding)), bodyBytes: new Uint8Array(Buffer.from(signed.bodyBytes, this.wcEncoding)), }, signature, }; } } exports.WCClient = WCClient;