Skip to content

feat: passive wallet_sessionChanged listening for extension#198

Open
jiexi wants to merge 24 commits intomainfrom
jl/passively-listen-wallet_sessionChanged-extension
Open

feat: passive wallet_sessionChanged listening for extension#198
jiexi wants to merge 24 commits intomainfrom
jl/passively-listen-wallet_sessionChanged-extension

Conversation

@jiexi
Copy link
Member

@jiexi jiexi commented Mar 5, 2026

Explanation

Uses the DefaultTransport on init if no existing transport is stored, extension is found, and extensionIsPreferred. Ensures that wallet_sessionChanged events are listened to and MultichainClient.status is correctly updated.

To test, use window.ethereum.request({method: 'eth_requestAccounts}) in console to trigger the permission flow outside of the MMC lifecycle

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed, highlighting breaking changes as necessary
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

@jiexi jiexi requested a review from a team as a code owner March 5, 2026 19:49
Comment on lines -249 to -252
this.#notifyCallbacks({
method: 'wallet_sessionChanged',
params: walletSession,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not happen anymore in the refactor since its only happening in the public init and not the private one. This doesn't seem intentional?

Comment on lines -258 to -286
const response = await this.request({ method: 'wallet_getSession' });
const { sessionScopes } = response.result as SessionData;

if (Object.keys(sessionScopes).length > 0) {
return;
}

this.#notificationCallbacks.clear();

// Remove the message listener when disconnecting
if (this.#handleResponseListener) {
// eslint-disable-next-line no-restricted-globals
window.removeEventListener('message', this.#handleResponseListener);
this.#handleResponseListener = undefined;
}

// Remove the notification listener when disconnecting
if (this.#handleNotificationListener) {
// eslint-disable-next-line no-restricted-globals
window.removeEventListener('message', this.#handleNotificationListener);
this.#handleNotificationListener = undefined;
}

// Reject all pending requests
for (const [, request] of this.#pendingRequests) {
clearTimeout(request.timeout);
request.reject(new Error('Transport disconnected'));
}
this.#pendingRequests.clear();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help me understand why we don't need any of this cleanup anymore?

);
walletSession = sessionRequest.result as SessionData;
} catch {
// wallet_getSession may fail if extension is not ready; treat as no session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should add a logger here?

Comment on lines +305 to +309
await this.#setupDefaultTransport();
// We don't actually want getStoredTransport to return this transport
// since we aren't connecting it to the wallet with a CAIP session, only
// listening for wallet_sessionChanged events.
await this.storage.removeTransport();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Alternatively we could add a passive parameter to #setupDefaultTransport to skip the storage write?

// Passive init may fail if extension transport isn't ready
}
}
this.status = 'loaded';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up — there's a status flicker here. When init() fires, it emits wallet_sessionChanged synchronously through #notifyCallbacks, which hits #onTransportNotification and sets this.status to 'connected' or 'disconnected' (depending on scopes). Then this line immediately overwrites it with 'loaded'. Since the status setter emits stateChanged to consumers on every write, they'll see a quick connected → loaded (or disconnected → loaded) flash — two state change notifications in rapid succession where the first one is misleading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants