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
259 changes: 250 additions & 9 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ import {
} from '@metamask/seedless-onboarding-controller';
import { PRODUCT_TYPES } from '@metamask/subscription-controller';
import { isSnapId } from '@metamask/snaps-utils';
import { address } from '@solana/addresses';
Copy link

Choose a reason for hiding this comment

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

Bug: Unused and misplaced Solana address import

The address function is imported from @solana/addresses but never used in this file. All other occurrences of address in the file are object destructuring from parseCaipAccountId(), not calls to this imported function. This appears to be accidentally committed debug code or a copy-paste error.

Fix in Cursor Fix in Web

import {
findAtomicBatchSupportForChain,
checkEip7702Support,
Expand Down Expand Up @@ -1638,8 +1639,9 @@ export default class MetamaskController extends EventEmitter {
setupControllerEventSubscriptions() {
let lastSelectedAddress;
let lastSelectedSolanaAccountAddress;
let lastSelectedTronAccountAddress;

// this throws if there is no solana account... perhaps we should handle this better at the controller level
// this throws if there is no solana or Tron account... perhaps we should handle this better at the controller level
try {
lastSelectedSolanaAccountAddress =
this.accountsController.getSelectedMultichainAccount(
Expand All @@ -1648,6 +1650,14 @@ export default class MetamaskController extends EventEmitter {
} catch {
// noop
}
try {
lastSelectedTronAccountAddress =
this.accountsController.getSelectedMultichainAccount(
MultichainNetworks.TRON,
)?.address;
} catch {
// noop
}

this.controllerMessenger.subscribe(
'PreferencesController:stateChange',
Expand Down Expand Up @@ -1943,6 +1953,194 @@ export default class MetamaskController extends EventEmitter {
},
);

// wallet_notify for tron accountChanged when permission changes
this.controllerMessenger.subscribe(
`${this.permissionController.name}:stateChange`,
async (currentValue, previousValue) => {
const origins = uniq([...previousValue.keys(), ...currentValue.keys()]);
origins.forEach((origin) => {
const previousCaveatValue = previousValue.get(origin);
const currentCaveatValue = currentValue.get(origin);

const previousTronAccountChangedNotificationsEnabled = Boolean(
previousCaveatValue?.sessionProperties?.[
KnownSessionProperties.TronAccountChangedNotifications
],
);
const currentTronAccountChangedNotificationsEnabled = Boolean(
currentCaveatValue?.sessionProperties?.[
KnownSessionProperties.TronAccountChangedNotifications
],
);

if (
!previousTronAccountChangedNotificationsEnabled &&
!currentTronAccountChangedNotificationsEnabled
) {
return;
}

const previousTronCaipAccountIds = previousCaveatValue
? getPermittedAccountsForScopes(previousCaveatValue, [
MultichainNetworks.TRON,
MultichainNetworks.TRON_SHASTA,
MultichainNetworks.TRON_NILE,
])
: [];
const previousNonUniqueTronHexAccountAddresses =
previousTronCaipAccountIds.map((caipAccountId) => {
const { address } = parseCaipAccountId(caipAccountId);
return address;
});
const previousTronHexAccountAddresses = uniq(
previousNonUniqueTronHexAccountAddresses,
);
const [previousSelectedTronAccountAddress] =
this.sortMultichainAccountsByLastSelected(
previousTronHexAccountAddresses,
);

const currentTronCaipAccountIds = currentCaveatValue
? getPermittedAccountsForScopes(currentCaveatValue, [
MultichainNetworks.TRON,
MultichainNetworks.TRON_SHASTA,
MultichainNetworks.TRON_NILE,
])
: [];
const currentNonUniqueTronHexAccountAddresses =
currentTronCaipAccountIds.map((caipAccountId) => {
const { address } = parseCaipAccountId(caipAccountId);
return address;
});
const currentTronHexAccountAddresses = uniq(
currentNonUniqueTronHexAccountAddresses,
);
const [currentSelectedTronAccountAddress] =
this.sortMultichainAccountsByLastSelected(
currentTronHexAccountAddresses,
);

if (
previousSelectedTronAccountAddress !==
currentSelectedTronAccountAddress
) {
this._notifyTronAccountChange(
origin,
currentSelectedTronAccountAddress
? [currentSelectedTronAccountAddress]
: [],
);
}
});
},
getAuthorizedScopesByOrigin,
);

// TODO: To be removed when state 2 is fully transitioned.
// wallet_notify for tron accountChanged when selected account changes
this.controllerMessenger.subscribe(
`${this.accountsController.name}:selectedAccountChange`,
async (account) => {
if (
account.type === TrxAccountType.Eoa &&
Copy link

Choose a reason for hiding this comment

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

Bug: Tron types used outside conditional compilation guard

The code uses TrxAccountType.Eoa and TrxScope.Mainnet outside any conditional compilation block, but these types are imported within ///: BEGIN:ONLY_INCLUDE_IF(tron) guards (lines 66-69). When the tron feature flag is disabled, these identifiers will be undefined, causing runtime errors when comparing account.type === TrxAccountType.Eoa or calling getAccountsFromSelectedAccountGroup with scopes: [TrxScope.Mainnet]. The new Tron subscription code needs to be wrapped in the same conditional blocks.

Additional Locations (2)

Fix in Cursor Fix in Web

account.address !== lastSelectedTronAccountAddress
) {
lastSelectedTronAccountAddress = account.address;

const originsWithTronAccountChangedNotifications =
getOriginsWithSessionProperty(
this.permissionController.state,
KnownSessionProperties.TronAccountChangedNotifications,
);

// returns a map of origins to permitted tron accounts
const tronAccounts = getPermittedAccountsForScopesByOrigin(
this.permissionController.state,
[
MultichainNetworks.TRON,
MultichainNetworks.TRON_SHASTA,
MultichainNetworks.TRON_NILE,
],
);

if (tronAccounts.size > 0) {
for (const [origin, accounts] of tronAccounts.entries()) {
const parsedTronAddresses = accounts.map((caipAccountId) => {
const { address } = parseCaipAccountId(caipAccountId);
return address;
});

if (
parsedTronAddresses.includes(account.address) &&
originsWithTronAccountChangedNotifications[origin]
) {
this._notifyTronAccountChange(origin, [account.address]);
}
}
}
}
},
);

// wallet_notify for tron accountChanged when selected account group changes
this.controllerMessenger.subscribe(
`${this.accountTreeController.name}:selectedAccountGroupChange`,
(groupId) => {
// TODO: Move this logic to the SnapKeyring directly.
// Forward selected accounts to the Snap keyring, so each Snaps can fetch those accounts.
// eslint-disable-next-line no-void
void this.forwardSelectedAccountGroupToSnapKeyring(
this.getSnapKeyringIfAvailable(),
groupId,
);

const [account] =
this.accountTreeController.getAccountsFromSelectedAccountGroup({
scopes: [TrxScope.Mainnet],
});
if (
account &&
account.type === TrxAccountType.Eoa &&
account.address !== lastSelectedTronAccountAddress
) {
lastSelectedTronAccountAddress = account.address;

const originsWithTronAccountChangedNotifications =
getOriginsWithSessionProperty(
this.permissionController.state,
KnownSessionProperties.TronAccountChangedNotifications,
);

// returns a map of origins to permitted tron accounts
const tronAccounts = getPermittedAccountsForScopesByOrigin(
this.permissionController.state,
[
MultichainNetworks.TRON,
MultichainNetworks.TRON_SHASTA,
MultichainNetworks.TRON_NILE,
],
);

if (tronAccounts.size > 0) {
for (const [origin, accounts] of tronAccounts.entries()) {
const parsedTronAddresses = accounts.map((caipAccountId) => {
const { address } = parseCaipAccountId(caipAccountId);
return address;
});

if (
parsedTronAddresses.includes(account.address) &&
originsWithTronAccountChangedNotifications[origin]
// originsWithTronAccountChangedNotifications[origin]
) {
this._notifyTronAccountChange(origin, [account.address]);
}
}
}
}
},
);

// TODO: Move this logic to the SnapKeyring directly.
// Forward selected accounts to the Snap keyring, so each Snaps can fetch those accounts.
this.controllerMessenger.subscribe(
Expand Down Expand Up @@ -6259,13 +6457,13 @@ export default class MetamaskController extends EventEmitter {
}

/**
* For origins with a solana scope permitted, sends a wallet_notify -> metamask_accountChanged
* event to fire for the solana scope with the currently selected solana account if any are
* For origins with a solana or tron scope permitted, sends a wallet_notify -> metamask_accountChanged
* event to fire for the scope with the currently selected account if any are
* permitted or empty array otherwise.
*
* @param {string} origin - The origin to notify with the current solana account
* @param {string} origin - The origin to notify with the current account
*/
notifySolanaAccountChangedForCurrentAccount(origin) {
notifyNonEVMAccountChangedForCurrentAccount(origin) {
let caip25Caveat;
try {
caip25Caveat = this.permissionController.getCaveat(
Expand All @@ -6292,10 +6490,16 @@ export default class MetamaskController extends EventEmitter {
KnownSessionProperties.SolanaAccountChangedNotifications
];

const tronAccountsChangedNotifications =
caip25Caveat.value.sessionProperties?.[
KnownSessionProperties.TronAccountChangedNotifications
];

const sessionScopes = getSessionScopes(caip25Caveat.value, {
getNonEvmSupportedMethods: this.getNonEvmSupportedMethods.bind(this),
});

// Handle Solana account notifications
const solanaScope =
sessionScopes[MultichainNetworks.SOLANA] ||
sessionScopes[MultichainNetworks.SOLANA_DEVNET] ||
Expand All @@ -6316,9 +6520,29 @@ export default class MetamaskController extends EventEmitter {
this._notifySolanaAccountChange(origin, [accountAddressToEmit]);
}
}
}

// ---------------------------------------------------------------------------
// Handle Tron account notifications
const tronScope =
sessionScopes[MultichainNetworks.TRON] ||
sessionScopes[MultichainNetworks.TRON_DEVNET] ||
sessionScopes[MultichainNetworks.TRON_TESTNET];
Copy link

Choose a reason for hiding this comment

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

Bug: Non-existent enum values for Tron testnet networks

The code references MultichainNetworks.TRON_DEVNET and MultichainNetworks.TRON_TESTNET, but these enum values don't exist in the MultichainNetworks enum. The actual testnet values are TRON_NILE and TRON_SHASTA. This will cause tronScope to be undefined for any Tron testnet sessions, preventing account change notifications from being sent for testnet connections. Other parts of the same codebase correctly use TRON_SHASTA and TRON_NILE.

Fix in Cursor Fix in Web


if (tronAccountsChangedNotifications && tronScope) {
const { accounts } = tronScope;
const parsedPermittedTronAddresses = accounts.map((caipAccountId) => {
const { address } = parseCaipAccountId(caipAccountId);
return address;
});

const [accountAddressToEmit] = this.sortMultichainAccountsByLastSelected(
parsedPermittedTronAddresses,
);

if (accountAddressToEmit) {
this._notifyTronAccountChange(origin, [accountAddressToEmit]);
}
}
} // ---------------------------------------------------------------------------
// Identity Management (signature operations)

getAddTransactionRequest({
Expand Down Expand Up @@ -6932,13 +7156,13 @@ export default class MetamaskController extends EventEmitter {
engine,
});

// solana account changed notifications
// solana and Tron account changed notifications
// This delay is needed because it's possible for a dapp to not have listeners
// setup in time right after a connection is established.
// This can be resolved if we amend the caip standards to include a liveliness
// handshake as part of the initial connection.
setTimeout(
() => this.notifySolanaAccountChangedForCurrentAccount(origin),
() => this.notifyNonEVMAccountChangedForCurrentAccount(origin),
500,
);

Expand Down Expand Up @@ -8677,6 +8901,23 @@ export default class MetamaskController extends EventEmitter {
);
}

async _notifyTronAccountChange(origin, accountAddressArray) {
this.notifyConnections(
origin,
{
method: MultichainApiNotifications.walletNotify,
params: {
scope: MultichainNetworks.TRON,
notification: {
method: NOTIFICATION_NAMES.accountsChanged,
params: accountAddressArray,
},
},
},
API_TYPE.CAIP_MULTICHAIN,
);
}

async _notifyChainChange() {
this.notifyAllConnections(
async (origin) => ({
Expand Down
Loading
Loading