Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 57 additions & 143 deletions packages/adapters/metamask-tron/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { TronScope } from '@metamask/multichain-api-client';
import {
type CaipAccountId,
type MultichainApiClient,
Expand All @@ -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,
Expand All @@ -24,7 +24,6 @@ import { Scope } from './types.js';
import {
chainIdToScope,
getAddressFromCaipAccountId,
isAccountChangedEvent,
scopeToChainId,
scopeToNetworkType,
isSessionChangedEvent,
Expand All @@ -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 =
Expand All @@ -56,9 +57,8 @@ export class MetaMaskAdapter extends AddonAdapter {
private _switchingChain = false;
private _address: string | null = null;
private _scope: Scope | undefined;
private _selectedAddressOnPageLoadPromise: Promise<string | undefined> | undefined;
private _checkWalletPromise: Promise<void> | undefined;
private _removeAccountsChangedListener: (() => void) | undefined;
private _removeSessionChangedListener: (() => void) | undefined;
private _transport: Transport;
private _client: MultichainApiClient;

Expand Down Expand Up @@ -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)
);
}
});
}
Expand Down Expand Up @@ -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) {
Expand All @@ -159,19 +165,22 @@ export class MetaMaskAdapter extends AddonAdapter {
* Disconnects from the MetaMask wallet.
* @returns A promise that resolves when disconnected.
*/
async disconnect(): Promise<void> {
async disconnect(options: { revokeSession?: boolean } = {}): Promise<void> {
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] });
}
}

/**
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<string | undefined> {
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.
Expand Down Expand Up @@ -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);
}
Expand All @@ -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<void> {
let resolvePromise: (value: string) => void;
const waitForAccountChangedPromise = new Promise<string>((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]: {
Expand All @@ -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<undefined>((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);
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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));
}
Expand Down
3 changes: 3 additions & 0 deletions packages/adapters/metamask-tron/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ export enum Scope {
NILE = 'tron:3448148188',
}

/**
* Type representing the string values of the Scope
*/
export type ScopeValue = `${Scope}`;
14 changes: 7 additions & 7 deletions packages/adapters/metamask-tron/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand All @@ -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;
Expand Down