Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: active session management for stale / removed sessions #356

Merged
merged 12 commits into from
Dec 3, 2024
Merged
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 { 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 { JsonRpcResult } from '@walletconnect/jsonrpc-types'

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

Check warning on line 57 in src/lib/dapp/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

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

Expand Down Expand Up @@ -91,7 +92,7 @@
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 @@
* 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 @@
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 @@
}

/**
* 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 @@
}

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 @@

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