diff --git a/packages/adapters/metamask-tron/src/adapter.ts b/packages/adapters/metamask-tron/src/adapter.ts index 20af004..da5ad1c 100644 --- a/packages/adapters/metamask-tron/src/adapter.ts +++ b/packages/adapters/metamask-tron/src/adapter.ts @@ -1,3 +1,4 @@ +import type { TronScope } from '@metamask/multichain-api-client'; import { type CaipAccountId, type MultichainApiClient, @@ -7,7 +8,6 @@ import { getMultichainClient, isMetamaskInstalled, } from '@metamask/multichain-api-client'; -import type { TronAddress } from '@metamask/multichain-api-client/dist/types/scopes/tron.types.cjs'; import { AdapterState, isInBrowser, @@ -24,7 +24,6 @@ import { Scope } from './types.js'; import { chainIdToScope, getAddressFromCaipAccountId, - isAccountChangedEvent, scopeToChainId, scopeToNetworkType, isSessionChangedEvent, @@ -43,6 +42,8 @@ export interface MetaMaskAdapterConfig extends BaseAdapterConfig { export const MetaMaskAdapterName = 'MetaMask' as AdapterName<'MetaMask'>; export class MetaMaskAdapter extends AddonAdapter { + // list of scopes in priority order for resolving selected account + readonly scopes = [Scope.MAINNET, Scope.SHASTA, Scope.NILE] as const; name = MetaMaskAdapterName; // @prettier-ignore icon = @@ -56,9 +57,8 @@ export class MetaMaskAdapter extends AddonAdapter { private _switchingChain = false; private _address: string | null = null; private _scope: Scope | undefined; - private _selectedAddressOnPageLoadPromise: Promise | undefined; private _checkWalletPromise: Promise | undefined; - private _removeAccountsChangedListener: (() => void) | undefined; + private _removeSessionChangedListener: (() => void) | undefined; private _transport: Transport; private _client: MultichainApiClient; @@ -97,6 +97,9 @@ export class MetaMaskAdapter extends AddonAdapter { .catch((error) => { console.warn('Failed to auto-restore session:', error); }); + this._removeSessionChangedListener = this._client.onNotification( + this.handleSessionChangedEvent.bind(this) + ); } }); } @@ -140,8 +143,11 @@ export class MetaMaskAdapter extends AddonAdapter { if (!this.address) { return; } - this.startListeners(); - + if (!this._removeSessionChangedListener) { + this._removeSessionChangedListener = this._client.onNotification( + this.handleSessionChangedEvent.bind(this) + ); + } this.setState(AdapterState.Connected); this.emit('connect', this.address); } catch (error: any) { @@ -159,19 +165,22 @@ export class MetaMaskAdapter extends AddonAdapter { * Disconnects from the MetaMask wallet. * @returns A promise that resolves when disconnected. */ - async disconnect(): Promise { + async disconnect(options: { revokeSession?: boolean } = {}): Promise { if (this.state !== AdapterState.Connected) { return; } - - this.stopListeners(); + const { revokeSession = true } = options; this.setAddress(null); this.setScope(undefined, false); this.setState(AdapterState.Disconnect); this.emit('disconnect'); - await this._client.revokeSession({ scopes: [Scope.MAINNET, Scope.NILE, Scope.SHASTA] }); + if (revokeSession) { + this._removeSessionChangedListener?.(); + this._removeSessionChangedListener = undefined; + await this._client.revokeSession({ scopes: [...this.scopes] }); + } } /** @@ -195,7 +204,7 @@ export class MetaMaskAdapter extends AddonAdapter { request: { method: 'signTransaction', params: { - address: this._address as TronAddress, + address: this._address as TronScope.TronAddress, transaction: { rawDataHex: transaction.raw_data_hex, type: contractType, @@ -235,7 +244,7 @@ export class MetaMaskAdapter extends AddonAdapter { scope: this._scope, request: { method: 'signMessage', - params: { message: base64Message, address: this._address as TronAddress }, + params: { message: base64Message, address: this._address as TronScope.TronAddress }, }, }); return result.signature; @@ -318,30 +327,6 @@ export class MetaMaskAdapter extends AddonAdapter { } } - /** - * Listen for up to 2 seconds to the accountsChanged event emitted on page load. - * @returns If any, the initial selected address. - */ - protected getInitialSelectedAddress(): Promise { - return new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve(undefined); - }, 2000); - const handleAccountChange = (data: any) => { - if (isAccountChangedEvent(data)) { - const address = data?.params?.notification?.params?.[0]; - if (address) { - clearTimeout(timeout); - removeNotification?.(); - resolve(address); - } - } - }; - - const removeNotification = this._client.onNotification(handleAccountChange); - }); - } - /** * Checks if the MetaMask wallet is available in the browser. * By default, the _readyState is set to Found to avoid issues on page reloads. @@ -398,10 +383,8 @@ export class MetaMaskAdapter extends AddonAdapter { if (!existingSession) { return; } - // Get the address from accountChanged emitted on page load, if any - const address = await this._selectedAddressOnPageLoadPromise; const scope = this.restoreScope(); - this.updateSession(existingSession, scope, address); + this.updateSession(existingSession, scope); } catch (error) { console.warn(`Error restoring session`, error); } @@ -413,26 +396,6 @@ export class MetaMaskAdapter extends AddonAdapter { * @param addresses - Optional list of addresses to include in the session. */ private async createSession(scope: Scope, addresses?: string[]): Promise { - let resolvePromise: (value: string) => void; - const waitForAccountChangedPromise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - // If there are multiple accounts, wait for the first accountChanged event to know which one to use - const handleAccountChange = (data: any) => { - if (!isAccountChangedEvent(data)) { - return; - } - const selectedAddress = data?.params?.notification?.params?.[0]; - - if (selectedAddress) { - removeNotification(); - resolvePromise(selectedAddress); - } - }; - - const removeNotification = this._client.onNotification(handleAccountChange); - const session = await this._client.createSession({ optionalScopes: { [scope]: { @@ -442,34 +405,27 @@ export class MetaMaskAdapter extends AddonAdapter { }, }, sessionProperties: { + // Previously this was needed to enable metamask_accountsChanged events for Solana. + // This isn't needed for that purpose since we now use wallet_sessionChanged events. + // However this is still needed to help the wallet identify our injected solana provider + // until we migrate to a more accurate property name. + // See: https://github.com/MetaMask/metamask-extension/blob/70dd748af54b58ceb8e78d227b6bdf118fb8e7ba/ui/pages/multichain-accounts/multichain-accounts-connect-page/multichain-accounts-connect-page.tsx#L169-L174 tron_accountChanged_notifications: true, }, }); - // Wait for the accountChanged event to know which one to use, timeout after 2000ms - const selectedAddress = await Promise.race([ - waitForAccountChangedPromise, - new Promise((resolve) => setTimeout(() => resolve(undefined), 2000)), - ]); - - this.updateSession(session, undefined, selectedAddress); + this.updateSession(session); } /** * Updates the session and the address to connect to. - * This method handles the logic for selecting the appropriate Tron network scope - * and address to connect to based on the following priority: - * 1. First tries to find an available scope in order: previously selected scope > mainnet > shasta > nile - * 2. For address selection: - * - First tries to use the selectedAddress param, most likely coming from - * the accountsChanged event - * - Falls back to the previously saved address if it exists in the scope - * - Finally defaults to the first address in the scope + * Selects the scope in priority order: previously selected scope > mainnet > shasta > nile, + * then uses the first account in that scope. * * @param session - The session data containing available scopes and accounts - * @param selectedAddress - The address that was selected by the user, if any + * @param selectedScope - The scope to prefer, if available */ - private updateSession(session: SessionData, selectedScope?: Scope, selectedAddress?: string) { + private updateSession(session: SessionData, selectedScope?: Scope) { const currentScope = this._scope; const scope = this.selectScopeFromSessionWithPriority(session, selectedScope); @@ -487,77 +443,39 @@ export class MetaMaskAdapter extends AddonAdapter { this.setAddress(null); return; } - let addressToConnect; - // Try to use selectedAddress - if (selectedAddress && scopeAccounts.includes(`${scope}:${selectedAddress}`)) { - addressToConnect = selectedAddress; - } - // Otherwise try to use the previously saved address in this._address - else if (this._address && scopeAccounts.includes(`${scope}:${this._address}`)) { - addressToConnect = this._address; - } - // Otherwise select first address - else { - addressToConnect = getAddressFromCaipAccountId(scopeAccounts[0]); - } - // Update the address and scope + const addressToConnect = getAddressFromCaipAccountId(scopeAccounts[0]); this.setAddress(addressToConnect); this.setScope(scope, currentScope !== scope); } /** - * Starts listening to the accountsChanged event. - * @param handler Optional custom handler for the event. - */ - private startListeners(handler?: (data: any) => void) { - this._removeAccountsChangedListener = this._client.onNotification(handler ?? this.handleEvents.bind(this)); - } - - /** - * Stops listening to the accountsChanged event. - */ - private stopListeners() { - this._removeAccountsChangedListener?.(); - this._removeAccountsChangedListener = undefined; - } - - /** - * Handles the accountsChanged event. + * Handles the wallet_sessionChanged event. * @param data - The event data */ - private async handleEvents(data: any) { - if (isAccountChangedEvent(data)) { - const newAddressSelected = data?.params?.notification?.params?.[0]; - if (!newAddressSelected) { - // Disconnect if no address selected - await this.disconnect(); - return; - } - const session = await this._client.getSession(); - if (!session) { - return; - } - this.updateSession(session, this._scope, newAddressSelected); - } else if (isSessionChangedEvent(data)) { - const session = data?.params; - if (!session) { - return; - } - const scope = this.selectScopeFromSessionWithPriority(session); + private async handleSessionChangedEvent(data: any) { + if (!isSessionChangedEvent(data)) { + return; + } - if (!scope) { - // Disconnect if no scope selected - await this.disconnect(); - return; - } - const isAccountsEmpty = !(session?.sessionScopes?.[scope]?.accounts?.length > 0); - if (isAccountsEmpty) { - // Disconnect if no address selected - await this.disconnect(); - return; - } - this.updateSession(session, scope); + const session = data?.params as SessionData; + if (!session) { + return; } + const scope = this.selectScopeFromSessionWithPriority(session); + + if (!scope) { + // Soft disconnect if no scope selected + await this.disconnect({ revokeSession: false }); + return; + } + const isAccountsEmpty = session.sessionScopes?.[scope]?.accounts?.[0] === undefined; + if (isAccountsEmpty) { + // Soft disconnect if no address selected + await this.disconnect({ revokeSession: false }); + return; + } + + this.updateSession(session, scope); } /** @@ -626,11 +544,7 @@ export class MetaMaskAdapter extends AddonAdapter { */ private selectScopeFromSessionWithPriority(session: SessionData, selectedScope?: Scope): Scope | undefined { const sessionScopes = new Set(Object.keys(session?.sessionScopes ?? {})); - const scopePriorityOrder = (selectedScope ? [selectedScope] : []).concat([ - Scope.MAINNET, - Scope.SHASTA, - Scope.NILE, - ]); + const scopePriorityOrder = (selectedScope ? [selectedScope] : []).concat(this.scopes); return scopePriorityOrder.find((scope) => sessionScopes.has(scope)); } diff --git a/packages/adapters/metamask-tron/src/types.ts b/packages/adapters/metamask-tron/src/types.ts index 2388367..5ea33ed 100644 --- a/packages/adapters/metamask-tron/src/types.ts +++ b/packages/adapters/metamask-tron/src/types.ts @@ -21,4 +21,7 @@ export enum Scope { NILE = 'tron:3448148188', } +/** + * Type representing the string values of the Scope + */ export type ScopeValue = `${Scope}`; diff --git a/packages/adapters/metamask-tron/src/utils.ts b/packages/adapters/metamask-tron/src/utils.ts index cafe5ba..07b6e41 100644 --- a/packages/adapters/metamask-tron/src/utils.ts +++ b/packages/adapters/metamask-tron/src/utils.ts @@ -55,14 +55,10 @@ export function getAddressFromCaipAccountId(caipAccountId: string): string { } /** - * Checks if the given data represents an accountsChanged event. - * @param data - The event data. - * @returns True if it's an accountsChanged event, false otherwise. + * Checks if the given data represents a sessionChanged event. + * @param event - The event data. + * @returns True if it's a sessionChanged event, false otherwise. */ -export function isAccountChangedEvent(event: any): boolean { - return event?.method === 'wallet_notify' && event?.params?.notification?.method === 'metamask_accountsChanged'; -} - export function isSessionChangedEvent(event: any): boolean { return event?.method === 'wallet_sessionChanged'; } @@ -86,6 +82,10 @@ export function scopeToNetworkType(scope: Scope): NetworkType { } } +/** + * Checks if the current environment is a MetaMask mobile webview. + * @returns True if it's a MetaMask mobile webview, false otherwise. + */ export function isMetaMaskMobileWebView() { if (typeof window === 'undefined') { return false;