Skip to content

Commit

Permalink
feat: active session management for stale / removed sessions (#356)
Browse files Browse the repository at this point in the history
There have been reports that sessions no longer exist despite a Signer still being available. This PR introduces a more active role in the library to automatically detect this kind of a mismatch.

It will remove a Signer when its session is no longer valid. A session might become invalid either by expiring, by a user disconnecting from the wallet, or some sort of a desync from IndexedDB.
Adds better typing for Log classes, and disables logs during test:watch to make it easier to debug tests.

Signed-off-by: Michael Kantor <[email protected]>
  • Loading branch information
kantorcodes authored Dec 3, 2024
1 parent 197895d commit d301015
Show file tree
Hide file tree
Showing 8 changed files with 899 additions and 143 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hashgraph/hedera-wallet-connect",
"version": "1.4.1",
"version": "1.4.2",
"description": "A library to facilitate integrating Hedera with WalletConnect",
"repository": {
"type": "git",
Expand Down
29 changes: 26 additions & 3 deletions src/lib/dapp/DAppSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ import {
Uint8ArrayToBase64String,
Uint8ArrayToString,
} from '../shared'
import { DefaultLogger, ILogger } from '../shared/logger'
import { DefaultLogger, ILogger, LogLevel } from '../shared/logger'
import { SessionNotFoundError } from './SessionNotFoundError'

const clients: Record<string, Client | null> = {}

Expand All @@ -73,7 +74,7 @@ export class DAppSigner implements Signer {
public readonly topic: string,
private readonly ledgerId: LedgerId = LedgerId.MAINNET,
public readonly extensionId?: string,
logLevel: 'error' | 'warn' | 'info' | 'debug' = 'debug',
logLevel: LogLevel = 'debug',
) {
this.logger = new DefaultLogger(logLevel)
}
Expand All @@ -82,7 +83,7 @@ export class DAppSigner implements Signer {
* Sets the logging level for the DAppSigner
* @param level - The logging level to set
*/
public setLogLevel(level: 'error' | 'warn' | 'info' | 'debug'): void {
public setLogLevel(level: LogLevel): void {
if (this.logger instanceof DefaultLogger) {
this.logger.setLogLevel(level)
}
Expand Down Expand Up @@ -116,6 +117,25 @@ export class DAppSigner implements Signer {
}

request<T>(request: { method: string; params: any }): Promise<T> {
// Avoid a wallet call if the session is no longer valid
if (!this?.signClient?.session?.get(this.topic)) {
this.logger.error(
'Session no longer exists, signer will be removed. Please reconnect to the wallet.',
)
// Notify DAppConnector to remove this signer
this.signClient.emit({
topic: this.topic,
event: {
name: 'session_delete',
data: { topic: this.topic },
},
chainId: ledgerIdToCAIPChainId(this.ledgerId),
})
throw new SessionNotFoundError(
'Session no longer exists. Please reconnect to the wallet.',
)
}

if (this.extensionId) extensionOpen(this.extensionId)
return this.signClient.request<T>({
topic: this.topic,
Expand Down Expand Up @@ -265,6 +285,7 @@ export class DAppSigner implements Signer {
return { result: TransactionResponse.fromJSON(result) as OutputT }
} catch (error) {
this.logger.error('Error executing transaction request:', error)

return { error }
}
}
Expand Down Expand Up @@ -356,10 +377,12 @@ export class DAppSigner implements Signer {
query: queryToBase64String(query),
},
})

this.logger.debug('Query request completed successfully', result)

return { result: this._parseQueryResponse(query, result.response) as OutputT }
} catch (error) {
this.logger.error('Error executing query request:', error)
return { error }
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/lib/dapp/SessionNotFoundError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class SessionNotFoundError extends Error {
constructor(message: string) {
super(message)
this.name = 'SessionNotFoundError'
}
}
55 changes: 42 additions & 13 deletions src/lib/dapp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import QRCodeModal from '@walletconnect/qrcode-modal'
import { WalletConnectModal } from '@walletconnect/modal'
import SignClient from '@walletconnect/sign-client'
import { getSdkError } from '@walletconnect/utils'
import { DefaultLogger, ILogger } from '../shared/logger'
import { DefaultLogger, ILogger, LogLevel } from '../shared/logger'
import {
HederaJsonRpcMethod,
accountAndLedgerFromSession,
Expand Down Expand Up @@ -54,6 +54,7 @@ import { DAppSigner } from './DAppSigner'
import { JsonRpcResult } from '@walletconnect/jsonrpc-types'

export * from './DAppSigner'
export { SessionNotFoundError } from './SessionNotFoundError'

type BaseLogger = 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'fatal'

Expand Down Expand Up @@ -91,7 +92,7 @@ export class DAppConnector {
methods?: string[],
events?: string[],
chains?: string[],
logLevel: 'error' | 'warn' | 'info' | 'debug' = 'debug',
logLevel: LogLevel = 'debug',
) {
this.logger = new DefaultLogger(logLevel)
this.dAppMetadata = metadata
Expand Down Expand Up @@ -120,7 +121,7 @@ export class DAppConnector {
* Sets the logging level for the DAppConnector
* @param level - The logging level to set
*/
public setLogLevel(level: 'error' | 'warn' | 'info' | 'debug'): void {
public setLogLevel(level: LogLevel): void {
if (this.logger instanceof DefaultLogger) {
this.logger.setLogLevel(level)
}
Expand Down Expand Up @@ -151,6 +152,11 @@ export class DAppConnector {
this.walletConnectClient.on('session_event', this.handleSessionEvent.bind(this))
this.walletConnectClient.on('session_update', this.handleSessionUpdate.bind(this))
this.walletConnectClient.on('session_delete', this.handleSessionDelete.bind(this))
// Listen for custom session_delete events from DAppSigner
this.walletConnectClient.core.events.on(
'session_delete',
this.handleSessionDelete.bind(this),
)
this.walletConnectClient.core.pairing.events.on(
'pairing_delete',
this.handlePairingDelete.bind(this),
Expand Down Expand Up @@ -269,9 +275,10 @@ export class DAppConnector {
}

/**
* Validates the session by checking if the session exists.
* Validates the session by checking if the session exists and is valid.
* Also ensures the signer exists for the session.
* @param topic - The topic of the session to validate.
* @returns {boolean} - True if the session exists, false otherwise.
* @returns {boolean} - True if the session exists and has a valid signer, false otherwise.
*/
private validateSession(topic: string): boolean {
try {
Expand All @@ -280,12 +287,24 @@ export class DAppConnector {
}

const session = this.walletConnectClient.session.get(topic)

const hasSigner = this.signers.some((signer) => signer.topic === topic)
if (!session) {
// If session doesn't exist but we have a signer for it, clean up
if (hasSigner) {
this.logger.warn(`Signer exists but no session found for topic: ${topic}`)
this.handleSessionDelete({ topic })
}
return false
}

if (!hasSigner) {
this.logger.warn(`Session exists but no signer found for topic: ${topic}`)
return false
}

return true
} catch {
} catch (e) {
this.logger.error('Error validating session:', e)
return false
}
}
Expand Down Expand Up @@ -687,13 +706,23 @@ export class DAppConnector {

private handleSessionDelete(event: { topic: string }) {
this.logger.info('Session deleted:', event)
this.signers = this.signers.filter((signer) => signer.topic !== event.topic)
try {
this.disconnect(event.topic)
} catch (e) {
this.logger.error('Error disconnecting session:', e)
let deletedSigner: boolean = false
this.signers = this.signers.filter((signer) => {
if (signer.topic !== event.topic) {
return true
}
deletedSigner = true
return false
})
// prevent emitting disconnected event if signers is untouched.
if (deletedSigner) {
try {
this.disconnect(event.topic)
} catch (e) {
this.logger.error('Error disconnecting session:', e)
}
this.logger.info('Session deleted and signer removed')
}
this.logger.info('Session deleted by wallet')
}

private handlePairingDelete(event: { topic: string }) {
Expand Down
10 changes: 6 additions & 4 deletions src/lib/shared/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ export interface ILogger {
debug(message: string, ...args: any[]): void
}

export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'off'

export class DefaultLogger implements ILogger {
private logLevel: 'error' | 'warn' | 'info' | 'debug' = 'info'
private logLevel: LogLevel = 'info'

constructor(logLevel: 'error' | 'warn' | 'info' | 'debug' = 'info') {
constructor(logLevel: LogLevel = 'info') {
this.logLevel = logLevel
}

setLogLevel(level: 'error' | 'warn' | 'info' | 'debug'): void {
setLogLevel(level: LogLevel): void {
this.logLevel = level
}

getLogLevel(): 'error' | 'warn' | 'info' | 'debug' {
getLogLevel(): LogLevel {
return this.logLevel
}

Expand Down
Loading

0 comments on commit d301015

Please sign in to comment.