From 396364442139700a643e33fa4938ba553052bb1c Mon Sep 17 00:00:00 2001 From: Nicholas Smith Date: Tue, 12 Nov 2024 11:16:25 -0600 Subject: [PATCH] feat: add staked ETH to metamask mobile homepage and account list menu (#12146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** 1. What is the reason for the change? We want to be able to view the Staked Ethereum balance on the homepage and have the balance totals update to reflect this 2. What is the improvement/solution? This PR adds the ability for the metamask-mobile app to view the Staked Ethereum asset balance in the mobile homepage in the Token List while linking to the ETH Asset Detail view. It also adds Staked Ethereum to the homepage total balance and the Account List menu total balances as well. The functionality is gated behind the `MM_POOLED_STAKING_UI_ENABLED` flag. ## **Related issues** Closes: https://consensyssoftware.atlassian.net/browse/STAKE-817 ## **Manual testing steps** 1. First launch the mobile application with `MM_POOLED_STAKING_UI_ENABLED="true"` in `js.env` 2. The homepage should show the `Staked Ethereum` asset balance for selected account in the homepage even if the balance is 0 3. On mainnet, the percentage +/- of the asset should be listed and should be the same as ETH 4. On holesky, the percentage +/- of the asset should not be listed 5. On an unsupported network, the Staked Ethereum asset should not show at all 6. If you click the Staked Ethereum asset it should go to the ETH asset detail page as Staked ETH is not a real token 7. On the ETH asset detail page, the Staked Ethereum balance should be the same as the homepage, **there should be no discrepancy** 8. Staking flow should not be affected 9. Total balance on the homepage should include Staked Ethereum balance 10. Total balances per account on the Account List menu dropdown should also include Staked Ethereum balance 11. All balanced mentioned should update when there is a balance change, one can go through staking flow to test this 12. **There should be no discrepancies across amounts as they have been addressed** ## **Screenshots/Recordings** ### **Before** WIP ### **After** Screenshot 2024-11-01 at 4 37 21 PM Screenshot 2024-11-01 at 4 37 27 PM Screenshot 2024-11-01 at 4 37 44 PM ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../UI/Stake/hooks/useBalance.test.tsx | 70 +- app/components/UI/Stake/hooks/useBalance.ts | 28 +- .../Tokens/TokenList/TokenListItem/index.tsx | 13 +- app/components/UI/Tokens/types.ts | 2 + app/components/Views/Wallet/index.tsx | 60 +- .../hooks/useAccounts/useAccounts.test.ts | 2 +- .../hooks/useAccounts/useAccounts.ts | 8 +- app/core/Engine.test.ts | 89 ++- app/core/Engine.ts | 18 +- e2e/specs/assets/import-tokens.spec.js | 2 + .../@metamask+assets-controllers+41.0.0.patch | 702 ++++++++++++++++++ 11 files changed, 916 insertions(+), 78 deletions(-) diff --git a/app/components/UI/Stake/hooks/useBalance.test.tsx b/app/components/UI/Stake/hooks/useBalance.test.tsx index aa227dfa3d1..e6bc9dc853d 100644 --- a/app/components/UI/Stake/hooks/useBalance.test.tsx +++ b/app/components/UI/Stake/hooks/useBalance.test.tsx @@ -7,8 +7,6 @@ import { backgroundState } from '../../../../util/test/initial-root-state'; import { renderHookWithProvider } from '../../../../util/test/renderWithProvider'; import useBalance from './useBalance'; import { toHex } from '@metamask/controller-utils'; -import usePooledStakes from './usePooledStakes'; -import { PooledStake } from '@metamask/stake-sdk'; const MOCK_ADDRESS_1 = '0x0'; @@ -24,7 +22,7 @@ const initialState = { AccountTrackerController: { accountsByChainId: { '0x1': { - [MOCK_ADDRESS_1]: { balance: toHex('12345678909876543210000000') }, + [MOCK_ADDRESS_1]: { balance: toHex('12345678909876543210000000'), stakedBalance: toHex(MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0].assets) }, }, }, }, @@ -40,25 +38,6 @@ const initialState = { }, }; -jest.mock('../hooks/usePooledStakes'); -const mockUsePooledStakes = ( - pooledStake: PooledStake, - exchangeRate: string, -) => { - (usePooledStakes as jest.MockedFn).mockReturnValue({ - pooledStakesData: pooledStake, - exchangeRate, - isLoadingPooledStakesData: false, - error: null, - refreshPooledStakes: jest.fn(), - hasStakedPositions: true, - hasEthToUnstake: true, - hasNeverStaked: false, - hasRewards: true, - hasRewardsOnly: false, - }); -}; - describe('useBalance', () => { afterEach(() => { jest.clearAllMocks(); @@ -69,10 +48,6 @@ describe('useBalance', () => { }); it('returns balance and fiat values based on account and pooled stake data', async () => { - mockUsePooledStakes( - MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0], - MOCK_GET_POOLED_STAKES_API_RESPONSE.exchangeRate, - ); const { result } = renderHookWithProvider(() => useBalance(), { state: initialState, }); @@ -90,10 +65,6 @@ describe('useBalance', () => { }); it('returns default values when no selected address and no account data', async () => { - mockUsePooledStakes( - MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0], - MOCK_GET_POOLED_STAKES_API_RESPONSE.exchangeRate, - ); const { result } = renderHookWithProvider(() => useBalance(), { state: { ...initialState, @@ -121,12 +92,39 @@ describe('useBalance', () => { }); it('returns correct stake amounts and fiat values based on account with high amount of assets', async () => { - mockUsePooledStakes( - MOCK_GET_POOLED_STAKES_API_RESPONSE_HIGH_ASSETS_AMOUNT.accounts[0], - MOCK_GET_POOLED_STAKES_API_RESPONSE_HIGH_ASSETS_AMOUNT.exchangeRate, - ); const { result } = renderHookWithProvider(() => useBalance(), { - state: initialState, + state: { + ...initialState, + engine: { + backgroundState: { + ...backgroundState, + AccountsController: createMockAccountsControllerState([ + MOCK_ADDRESS_1, + ]), + AccountTrackerController: { + accountsByChainId: { + '0x1': { + [MOCK_ADDRESS_1]: { + balance: toHex('12345678909876543210000000'), + stakedBalance: toHex( + MOCK_GET_POOLED_STAKES_API_RESPONSE_HIGH_ASSETS_AMOUNT + .accounts[0].assets, + ), + }, + }, + }, + }, + CurrencyRateController: { + currentCurrency: 'usd', + currencyRates: { + ETH: { + conversionRate: 3200, + }, + }, + }, + }, + }, + }, }); expect(result.current.balanceETH).toBe('12345678.90988'); // ETH balance @@ -139,6 +137,6 @@ describe('useBalance', () => { expect(result.current.stakedBalanceWei).toBe('99999999990000000000000'); // No staked assets expect(result.current.formattedStakedBalanceETH).toBe('99999.99999 ETH'); // Formatted ETH balance expect(result.current.stakedBalanceFiatNumber).toBe(319999999.968); // Staked balance in fiat number - expect(result.current.formattedStakedBalanceFiat).toBe('$319999999.97'); // + expect(result.current.formattedStakedBalanceFiat).toBe('$319999999.96'); // should round to floor }); }); diff --git a/app/components/UI/Stake/hooks/useBalance.ts b/app/components/UI/Stake/hooks/useBalance.ts index 6fccc8e0f83..7a012894b55 100644 --- a/app/components/UI/Stake/hooks/useBalance.ts +++ b/app/components/UI/Stake/hooks/useBalance.ts @@ -9,12 +9,10 @@ import { import { selectChainId } from '../../../../selectors/networkController'; import { hexToBN, - renderFiat, renderFromWei, weiToFiat, weiToFiatNumber, } from '../../../../util/number'; -import usePooledStakes from './usePooledStakes'; const useBalance = () => { const accountsByChainId = useSelector(selectAccountsByChainId); @@ -29,6 +27,10 @@ const useBalance = () => { ? accountsByChainId[chainId]?.[selectedAddress]?.balance : '0'; + const stakedBalance = selectedAddress + ? accountsByChainId[chainId]?.[selectedAddress]?.stakedBalance || '0' + : '0'; + const balanceETH = useMemo( () => renderFromWei(rawAccountBalance), [rawAccountBalance], @@ -49,21 +51,25 @@ const useBalance = () => { [balanceWei, conversionRate], ); - const { pooledStakesData } = usePooledStakes(); - const assets = hexToBN(pooledStakesData.assets).toString('hex'); const formattedStakedBalanceETH = useMemo( - () => `${renderFromWei(assets)} ETH`, - [assets], + () => `${renderFromWei(stakedBalance)} ETH`, + [stakedBalance], ); const stakedBalanceFiatNumber = useMemo( - () => weiToFiatNumber(assets, conversionRate), - [assets, conversionRate], + () => weiToFiatNumber(stakedBalance, conversionRate), + [stakedBalance, conversionRate], ); const formattedStakedBalanceFiat = useMemo( - () => renderFiat(stakedBalanceFiatNumber, currentCurrency, 2), - [currentCurrency, stakedBalanceFiatNumber], + () => weiToFiat( + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hexToBN(stakedBalance) as any, + conversionRate, + currentCurrency, + ), + [currentCurrency, stakedBalance, conversionRate], ); return { @@ -71,7 +77,7 @@ const useBalance = () => { balanceFiat, balanceWei, balanceFiatNumber, - stakedBalanceWei: assets ?? '0', + stakedBalanceWei: hexToBN(stakedBalance).toString(), formattedStakedBalanceETH, stakedBalanceFiatNumber, formattedStakedBalanceFiat, diff --git a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx index 651bebf7f7f..68c53c0576c 100644 --- a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx +++ b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx @@ -171,6 +171,10 @@ export const TokenListItem = ({ }; const onItemPress = (token: TokenI) => { + // if the asset is staked, navigate to the native asset details + if (asset.isStaked) { + return navigation.navigate('Asset', {...token.nativeAsset}); + } navigation.navigate('Asset', { ...token, }); @@ -178,7 +182,8 @@ export const TokenListItem = ({ return ( {asset.name || asset.symbol} - {/** Add button link to Portfolio Stake if token is mainnet ETH */} - {asset.isETH && isStakingSupportedChain && ( - - )} + {/** Add button link to Portfolio Stake if token is supported ETH chain and not a staked asset */} + {asset.isETH && isStakingSupportedChain && !asset.isStaked && } {!isTestNet(chainId) ? ( diff --git a/app/components/UI/Tokens/types.ts b/app/components/UI/Tokens/types.ts index 87bf7db218f..76bdef78718 100644 --- a/app/components/UI/Tokens/types.ts +++ b/app/components/UI/Tokens/types.ts @@ -21,4 +21,6 @@ export interface TokenI { balanceFiat: string; logo: string | undefined; isETH: boolean | undefined; + isStaked?: boolean | undefined; + nativeAsset?: TokenI | undefined; } diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 864beb3876d..2036f343925 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -530,34 +530,58 @@ const Wallet = ({ // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let balance: any = 0; - let assets = tokens; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let stakedBalance: any = 0; + + const assets = [ + ...(tokens || []), + ]; if (accountBalanceByChainId) { - balance = renderFromWei(accountBalanceByChainId.balance); - assets = [ - { - // TODO: Add name property to Token interface in controllers. - name: getTicker(ticker) === 'ETH' ? 'Ethereum' : ticker, - symbol: getTicker(ticker), - isETH: true, - balance, + balance = renderFromWei(accountBalanceByChainId.balance); + const nativeAsset = { + // TODO: Add name property to Token interface in controllers. + name: getTicker(ticker) === 'ETH' ? 'Ethereum' : ticker, + symbol: getTicker(ticker), + isETH: true, + balance, + balanceFiat: weiToFiat( + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hexToBN(accountBalanceByChainId.balance) as any, + conversionRate, + currentCurrency, + ), + logo: '../images/eth-logo-new.png', + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + assets.push(nativeAsset); + + let stakedAsset; + if (accountBalanceByChainId.stakedBalance) { + stakedBalance = renderFromWei(accountBalanceByChainId.stakedBalance); + stakedAsset = { + ...nativeAsset, + nativeAsset, + name: 'Staked Ethereum', + isStaked: true, + balance: stakedBalance, balanceFiat: weiToFiat( // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - hexToBN(accountBalanceByChainId.balance) as any, + hexToBN(accountBalanceByChainId.stakedBalance) as any, conversionRate, currentCurrency, ), - logo: '../images/eth-logo-new.png', - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, - ...(tokens || []), - ]; - } else { - assets = tokens; + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + assets.push(stakedAsset); + } } + return ( { const ticker = 'ETH'; const ethConversionRate = 4000; // $4,000 / ETH const ethBalance = 1; + const stakedEthBalance = 1; const state: Partial = { AccountsController: createMockAccountsControllerState( @@ -121,7 +122,6 @@ describe('Engine', () => { id: '0x1', nickname: 'mainnet', ticker: 'ETH', - type: RpcEndpointType.Infura, }), }, // TODO(dbrans): Investigate why the shape of the NetworkController state in this @@ -252,6 +252,91 @@ describe('Engine', () => { tokenFiat1dAgo, }); }); + + it('calculates when there is ETH and staked ETH and tokens', () => { + const ethPricePercentChange1d = 5; + + const tokens = [ + { + address: '0x001', + balance: 1, + price: '1', + pricePercentChange1d: -1, + }, + { + address: '0x002', + balance: 2, + price: '2', + pricePercentChange1d: 2, + }, + ]; + + engine = Engine.init({ + ...state, + AccountTrackerController: { + accountsByChainId: { + [chainId]: { + [selectedAddress]: { balance: (ethBalance * 1e18).toString(), stakedBalance: (stakedEthBalance * 1e18).toString() }, + }, + }, + accounts: { + [selectedAddress]: { balance: (ethBalance * 1e18).toString(), stakedBalance: (stakedEthBalance * 1e18).toString() }, + }, + }, + TokensController: { + tokens: tokens.map((token) => ({ + address: token.address, + balance: token.balance, + decimals: 18, + symbol: 'TEST', + })), + ignoredTokens: [], + detectedTokens: [], + allTokens: {}, + allIgnoredTokens: {}, + allDetectedTokens: {}, + }, + TokenRatesController: { + marketData: { + [chainId]: { + [zeroAddress()]: { + pricePercentChange1d: ethPricePercentChange1d, + }, + ...tokens.reduce( + (acc, token) => ({ + ...acc, + [token.address]: { + price: token.price, + pricePercentChange1d: token.pricePercentChange1d, + }, + }), + {}, + ), + }, + }, + }, + }); + + const totalFiatBalance = engine.getTotalFiatAccountBalance(); + const ethFiat = (ethBalance + stakedEthBalance) * ethConversionRate; + const [tokenFiat, tokenFiat1dAgo] = tokens.reduce( + ([fiat, fiat1d], token) => { + const value = Number(token.price) * token.balance * ethConversionRate; + return [ + fiat + value, + fiat1d + value / (1 + token.pricePercentChange1d / 100), + ]; + }, + [0, 0], + ); + + expect(totalFiatBalance).toStrictEqual({ + ethFiat, + ethFiat1dAgo: ethFiat / (1 + ethPricePercentChange1d / 100), + tokenFiat, + tokenFiat1dAgo, + }); + }); }); }); diff --git a/app/core/Engine.ts b/app/core/Engine.ts index d488819e7cf..976ecc5a198 100644 --- a/app/core/Engine.ts +++ b/app/core/Engine.ts @@ -173,6 +173,7 @@ import { weiToFiatNumber, toHexadecimal, addHexPrefix, + hexToBN, } from '../util/number'; import NotificationManager from './NotificationManager'; import Logger from '../util/Logger'; @@ -266,6 +267,7 @@ import { getSmartTransactionMetricsProperties } from '../util/smart-transactions import { trace } from '../util/trace'; import { MetricsEventBuilder } from './Analytics/MetricsEventBuilder'; import { JsonMap } from './Analytics/MetaMetrics.types'; +import { isPooledStakingFeatureEnabled } from '../components/UI/Stake/constants'; const NON_EMPTY = 'NON_EMPTY'; @@ -957,6 +959,11 @@ export class Engine { ], }), state: initialState.AccountTrackerController ?? { accounts: {} }, + getStakedBalanceForChain: + assetsContractController.getStakedBalanceForChain.bind( + assetsContractController, + ), + includeStakedAssets: isPooledStakingFeatureEnabled(), }); const permissionController = new PermissionController({ // @ts-expect-error TODO: Resolve mismatch between base-controller versions. @@ -1991,10 +1998,15 @@ export class Engine { selectSelectedInternalAccountChecksummedAddress ] ) { + const balanceBN = hexToBN(accountsByChainId[toHexadecimal(chainId)][ + selectSelectedInternalAccountChecksummedAddress + ].balance); + const stakedBalanceBN = hexToBN(accountsByChainId[toHexadecimal(chainId)][ + selectSelectedInternalAccountChecksummedAddress + ].stakedBalance || '0x00'); + const totalAccountBalance = balanceBN.add(stakedBalanceBN).toString('hex'); ethFiat = weiToFiatNumber( - accountsByChainId[toHexadecimal(chainId)][ - selectSelectedInternalAccountChecksummedAddress - ].balance, + totalAccountBalance, conversionRate, decimalsToShow, ); diff --git a/e2e/specs/assets/import-tokens.spec.js b/e2e/specs/assets/import-tokens.spec.js index 1d213d8fba3..cd6b3026ac9 100644 --- a/e2e/specs/assets/import-tokens.spec.js +++ b/e2e/specs/assets/import-tokens.spec.js @@ -70,6 +70,8 @@ describe(SmokeAssets('Import Tokens'), () => { }); it('should add a token via token footer link', async () => { + await TestHelpers.delay(2000); // Wait for the footer link to be visible + await WalletView.tapImportTokensFooterLink(); await ImportTokensView.searchToken('SNX'); await ImportTokensView.tapOnToken(); // taps the first token in the returned list diff --git a/patches/@metamask+assets-controllers+41.0.0.patch b/patches/@metamask+assets-controllers+41.0.0.patch index 58f5691868e..30bedc1ce68 100644 --- a/patches/@metamask+assets-controllers+41.0.0.patch +++ b/patches/@metamask+assets-controllers+41.0.0.patch @@ -1,3 +1,614 @@ +diff --git a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.cjs b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.cjs +index aac84e5..983c3f1 100644 +--- a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.cjs ++++ b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.cjs +@@ -1,19 +1,19 @@ + "use strict"; +-var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { +- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); +- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); +- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +-}; + var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; + }; ++var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { ++ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); ++ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); ++ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); ++}; + var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; +-var _AccountTrackerController_instances, _AccountTrackerController_refreshMutex, _AccountTrackerController_handle, _AccountTrackerController_getCurrentChainId, _AccountTrackerController_getCorrectNetworkClient, _AccountTrackerController_getBalanceFromChain; ++var _AccountTrackerController_instances, _AccountTrackerController_refreshMutex, _AccountTrackerController_includeStakedAssets, _AccountTrackerController_getStakedBalanceForChain, _AccountTrackerController_handle, _AccountTrackerController_getCurrentChainId, _AccountTrackerController_getCorrectNetworkClient, _AccountTrackerController_getBalanceFromChain; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.AccountTrackerController = void 0; + const controller_utils_1 = require("@metamask/controller-utils"); +@@ -47,8 +47,10 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo + * @param options.interval - Polling interval used to fetch new account balances. + * @param options.state - Initial state to set on this controller. + * @param options.messenger - The controller messaging system. ++ * @param options.getStakedBalanceForChain - The function to get the staked native asset balance for a chain. ++ * @param options.includeStakedAssets - Whether to include staked assets in the account balances. + */ +- constructor({ interval = 10000, state, messenger, }) { ++ constructor({ interval = 10000, state, messenger, getStakedBalanceForChain, includeStakedAssets = false, }) { + const { selectedNetworkClientId } = messenger.call('NetworkController:getState'); + const { configuration: { chainId }, } = messenger.call('NetworkController:getNetworkClientById', selectedNetworkClientId); + super({ +@@ -65,7 +67,11 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo + }); + _AccountTrackerController_instances.add(this); + _AccountTrackerController_refreshMutex.set(this, new async_mutex_1.Mutex()); ++ _AccountTrackerController_includeStakedAssets.set(this, void 0); ++ _AccountTrackerController_getStakedBalanceForChain.set(this, void 0); + _AccountTrackerController_handle.set(this, void 0); ++ __classPrivateFieldSet(this, _AccountTrackerController_getStakedBalanceForChain, getStakedBalanceForChain, "f"); ++ __classPrivateFieldSet(this, _AccountTrackerController_includeStakedAssets, includeStakedAssets, "f"); + this.setIntervalLength(interval); + // TODO: Either fix this lint violation or explain why it's necessary to ignore. + // eslint-disable-next-line @typescript-eslint/no-floating-promises +@@ -171,6 +177,15 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo + balance, + }; + } ++ if (__classPrivateFieldGet(this, _AccountTrackerController_includeStakedAssets, "f")) { ++ const stakedBalance = await __classPrivateFieldGet(this, _AccountTrackerController_getStakedBalanceForChain, "f").call(this, address, networkClientId); ++ if (stakedBalance) { ++ accountsForChain[address] = { ++ ...accountsForChain[address], ++ stakedBalance, ++ }; ++ } ++ } + } + this.update((state) => { + if (chainId === __classPrivateFieldGet(this, _AccountTrackerController_instances, "m", _AccountTrackerController_getCurrentChainId).call(this)) { +@@ -196,18 +211,23 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo + return (0, controller_utils_1.safelyExecuteWithTimeout)(async () => { + (0, utils_1.assert)(ethQuery, 'Provider not set.'); + const balance = await (0, controller_utils_1.query)(ethQuery, 'getBalance', [address]); +- return [address, balance]; ++ let stakedBalance; ++ if (__classPrivateFieldGet(this, _AccountTrackerController_includeStakedAssets, "f")) { ++ stakedBalance = await __classPrivateFieldGet(this, _AccountTrackerController_getStakedBalanceForChain, "f").call(this, address, networkClientId); ++ } ++ return [address, balance, stakedBalance]; + }); + })).then((value) => { + return value.reduce((obj, item) => { + if (!item) { + return obj; + } +- const [address, balance] = item; ++ const [address, balance, stakedBalance] = item; + return { + ...obj, + [address]: { + balance, ++ stakedBalance, + }, + }; + }, {}); +@@ -215,7 +235,7 @@ class AccountTrackerController extends (0, polling_controller_1.StaticIntervalPo + } + } + exports.AccountTrackerController = AccountTrackerController; +-_AccountTrackerController_refreshMutex = new WeakMap(), _AccountTrackerController_handle = new WeakMap(), _AccountTrackerController_instances = new WeakSet(), _AccountTrackerController_getCurrentChainId = function _AccountTrackerController_getCurrentChainId() { ++_AccountTrackerController_refreshMutex = new WeakMap(), _AccountTrackerController_includeStakedAssets = new WeakMap(), _AccountTrackerController_getStakedBalanceForChain = new WeakMap(), _AccountTrackerController_handle = new WeakMap(), _AccountTrackerController_instances = new WeakSet(), _AccountTrackerController_getCurrentChainId = function _AccountTrackerController_getCurrentChainId() { + const { selectedNetworkClientId } = this.messagingSystem.call('NetworkController:getState'); + const { configuration: { chainId }, } = this.messagingSystem.call('NetworkController:getNetworkClientById', selectedNetworkClientId); + return chainId; +diff --git a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.d.cts b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.d.cts +index 144e018..6b7d8cd 100644 +--- a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.d.cts ++++ b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.d.cts +@@ -2,6 +2,7 @@ import type { AccountsControllerSelectedEvmAccountChangeEvent, AccountsControlle + import type { ControllerStateChangeEvent, ControllerGetStateAction, RestrictedControllerMessenger } from "@metamask/base-controller"; + import type { NetworkClientId, NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction } from "@metamask/network-controller"; + import type { PreferencesControllerGetStateAction } from "@metamask/preferences-controller"; ++import type { AssetsContractController, StakedBalance } from "./AssetsContractController.cjs"; + /** + * The name of the {@link AccountTrackerController}. + */ +@@ -10,10 +11,12 @@ declare const controllerName = "AccountTrackerController"; + * @type AccountInformation + * + * Account information object +- * @property balance - Hex string of an account balancec in wei ++ * @property balance - Hex string of an account balance in wei ++ * @property stakedBalance - Hex string of an account staked balance in wei + */ + export type AccountInformation = { + balance: string; ++ stakedBalance?: string; + }; + /** + * @type AccountTrackerControllerState +@@ -62,18 +65,15 @@ type AccountTrackerPollingInput = { + networkClientId: NetworkClientId; + }; + declare const AccountTrackerController_base: (abstract new (...args: any[]) => { +- readonly "__#787890@#intervalIds": Record; +- "__#787890@#intervalLength": number | undefined; ++ readonly "__#784968@#intervalIds": Record; ++ "__#784968@#intervalLength": number | undefined; + setIntervalLength(intervalLength: number): void; + getIntervalLength(): number | undefined; + _startPolling(input: AccountTrackerPollingInput): void; + _stopPollingByPollingTokenSetId(key: string): void; +- readonly "__#787882@#pollingTokenSets": Map>; +- "__#787882@#callbacks": Map void>>; ++ readonly "__#784960@#pollingTokenSets": Map>; ++ "__#784960@#callbacks": Map void>>; + _executePoll(input: AccountTrackerPollingInput): Promise; +- /** +- * The action that can be performed to get the state of the {@link AccountTrackerController}. +- */ + startPolling(input: AccountTrackerPollingInput): string; + stopAllPolling(): void; + stopPollingByPollingToken(pollingToken: string): void; +@@ -91,11 +91,15 @@ export declare class AccountTrackerController extends AccountTrackerController_b + * @param options.interval - Polling interval used to fetch new account balances. + * @param options.state - Initial state to set on this controller. + * @param options.messenger - The controller messaging system. ++ * @param options.getStakedBalanceForChain - The function to get the staked native asset balance for a chain. ++ * @param options.includeStakedAssets - Whether to include staked assets in the account balances. + */ +- constructor({ interval, state, messenger, }: { ++ constructor({ interval, state, messenger, getStakedBalanceForChain, includeStakedAssets, }: { + interval?: number; + state?: Partial; + messenger: AccountTrackerControllerMessenger; ++ getStakedBalanceForChain: AssetsContractController['getStakedBalanceForChain']; ++ includeStakedAssets?: boolean; + }); + private syncAccounts; + /** +@@ -128,6 +132,7 @@ export declare class AccountTrackerController extends AccountTrackerController_b + */ + syncBalanceWithAddresses(addresses: string[], networkClientId?: NetworkClientId): Promise>; + } + export default AccountTrackerController; +diff --git a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.d.mts b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.d.mts +index b25212c..20d566f 100644 +--- a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.d.mts ++++ b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.d.mts +@@ -2,6 +2,7 @@ import type { AccountsControllerSelectedEvmAccountChangeEvent, AccountsControlle + import type { ControllerStateChangeEvent, ControllerGetStateAction, RestrictedControllerMessenger } from "@metamask/base-controller"; + import type { NetworkClientId, NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction } from "@metamask/network-controller"; + import type { PreferencesControllerGetStateAction } from "@metamask/preferences-controller"; ++import type { AssetsContractController, StakedBalance } from "./AssetsContractController.mjs"; + /** + * The name of the {@link AccountTrackerController}. + */ +@@ -10,10 +11,12 @@ declare const controllerName = "AccountTrackerController"; + * @type AccountInformation + * + * Account information object +- * @property balance - Hex string of an account balancec in wei ++ * @property balance - Hex string of an account balance in wei ++ * @property stakedBalance - Hex string of an account staked balance in wei + */ + export type AccountInformation = { + balance: string; ++ stakedBalance?: string; + }; + /** + * @type AccountTrackerControllerState +@@ -62,18 +65,15 @@ type AccountTrackerPollingInput = { + networkClientId: NetworkClientId; + }; + declare const AccountTrackerController_base: (abstract new (...args: any[]) => { +- readonly "__#787890@#intervalIds": Record; +- "__#787890@#intervalLength": number | undefined; ++ readonly "__#784968@#intervalIds": Record; ++ "__#784968@#intervalLength": number | undefined; + setIntervalLength(intervalLength: number): void; + getIntervalLength(): number | undefined; + _startPolling(input: AccountTrackerPollingInput): void; + _stopPollingByPollingTokenSetId(key: string): void; +- readonly "__#787882@#pollingTokenSets": Map>; +- "__#787882@#callbacks": Map void>>; ++ readonly "__#784960@#pollingTokenSets": Map>; ++ "__#784960@#callbacks": Map void>>; + _executePoll(input: AccountTrackerPollingInput): Promise; +- /** +- * The action that can be performed to get the state of the {@link AccountTrackerController}. +- */ + startPolling(input: AccountTrackerPollingInput): string; + stopAllPolling(): void; + stopPollingByPollingToken(pollingToken: string): void; +@@ -91,11 +91,15 @@ export declare class AccountTrackerController extends AccountTrackerController_b + * @param options.interval - Polling interval used to fetch new account balances. + * @param options.state - Initial state to set on this controller. + * @param options.messenger - The controller messaging system. ++ * @param options.getStakedBalanceForChain - The function to get the staked native asset balance for a chain. ++ * @param options.includeStakedAssets - Whether to include staked assets in the account balances. + */ +- constructor({ interval, state, messenger, }: { ++ constructor({ interval, state, messenger, getStakedBalanceForChain, includeStakedAssets, }: { + interval?: number; + state?: Partial; + messenger: AccountTrackerControllerMessenger; ++ getStakedBalanceForChain: AssetsContractController['getStakedBalanceForChain']; ++ includeStakedAssets?: boolean; + }); + private syncAccounts; + /** +@@ -128,6 +132,7 @@ export declare class AccountTrackerController extends AccountTrackerController_b + */ + syncBalanceWithAddresses(addresses: string[], networkClientId?: NetworkClientId): Promise>; + } + export default AccountTrackerController; +diff --git a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.mjs b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.mjs +index 4c4bf70..48cb006 100644 +--- a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.mjs ++++ b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.mjs +@@ -1,15 +1,15 @@ +-var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { +- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); +- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); +- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +-}; + var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; + }; +-var _AccountTrackerController_instances, _AccountTrackerController_refreshMutex, _AccountTrackerController_handle, _AccountTrackerController_getCurrentChainId, _AccountTrackerController_getCorrectNetworkClient, _AccountTrackerController_getBalanceFromChain; ++var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { ++ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); ++ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); ++ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); ++}; ++var _AccountTrackerController_instances, _AccountTrackerController_refreshMutex, _AccountTrackerController_includeStakedAssets, _AccountTrackerController_getStakedBalanceForChain, _AccountTrackerController_handle, _AccountTrackerController_getCurrentChainId, _AccountTrackerController_getCorrectNetworkClient, _AccountTrackerController_getBalanceFromChain; + function $importDefault(module) { + if (module?.__esModule) { + return module.default; +@@ -49,8 +49,10 @@ export class AccountTrackerController extends StaticIntervalPollingController() + * @param options.interval - Polling interval used to fetch new account balances. + * @param options.state - Initial state to set on this controller. + * @param options.messenger - The controller messaging system. ++ * @param options.getStakedBalanceForChain - The function to get the staked native asset balance for a chain. ++ * @param options.includeStakedAssets - Whether to include staked assets in the account balances. + */ +- constructor({ interval = 10000, state, messenger, }) { ++ constructor({ interval = 10000, state, messenger, getStakedBalanceForChain, includeStakedAssets = false, }) { + const { selectedNetworkClientId } = messenger.call('NetworkController:getState'); + const { configuration: { chainId }, } = messenger.call('NetworkController:getNetworkClientById', selectedNetworkClientId); + super({ +@@ -67,7 +69,11 @@ export class AccountTrackerController extends StaticIntervalPollingController() + }); + _AccountTrackerController_instances.add(this); + _AccountTrackerController_refreshMutex.set(this, new Mutex()); ++ _AccountTrackerController_includeStakedAssets.set(this, void 0); ++ _AccountTrackerController_getStakedBalanceForChain.set(this, void 0); + _AccountTrackerController_handle.set(this, void 0); ++ __classPrivateFieldSet(this, _AccountTrackerController_getStakedBalanceForChain, getStakedBalanceForChain, "f"); ++ __classPrivateFieldSet(this, _AccountTrackerController_includeStakedAssets, includeStakedAssets, "f"); + this.setIntervalLength(interval); + // TODO: Either fix this lint violation or explain why it's necessary to ignore. + // eslint-disable-next-line @typescript-eslint/no-floating-promises +@@ -173,6 +179,15 @@ export class AccountTrackerController extends StaticIntervalPollingController() + balance, + }; + } ++ if (__classPrivateFieldGet(this, _AccountTrackerController_includeStakedAssets, "f")) { ++ const stakedBalance = await __classPrivateFieldGet(this, _AccountTrackerController_getStakedBalanceForChain, "f").call(this, address, networkClientId); ++ if (stakedBalance) { ++ accountsForChain[address] = { ++ ...accountsForChain[address], ++ stakedBalance, ++ }; ++ } ++ } + } + this.update((state) => { + if (chainId === __classPrivateFieldGet(this, _AccountTrackerController_instances, "m", _AccountTrackerController_getCurrentChainId).call(this)) { +@@ -198,25 +213,30 @@ export class AccountTrackerController extends StaticIntervalPollingController() + return safelyExecuteWithTimeout(async () => { + assert(ethQuery, 'Provider not set.'); + const balance = await query(ethQuery, 'getBalance', [address]); +- return [address, balance]; ++ let stakedBalance; ++ if (__classPrivateFieldGet(this, _AccountTrackerController_includeStakedAssets, "f")) { ++ stakedBalance = await __classPrivateFieldGet(this, _AccountTrackerController_getStakedBalanceForChain, "f").call(this, address, networkClientId); ++ } ++ return [address, balance, stakedBalance]; + }); + })).then((value) => { + return value.reduce((obj, item) => { + if (!item) { + return obj; + } +- const [address, balance] = item; ++ const [address, balance, stakedBalance] = item; + return { + ...obj, + [address]: { + balance, ++ stakedBalance, + }, + }; + }, {}); + }); + } + } +-_AccountTrackerController_refreshMutex = new WeakMap(), _AccountTrackerController_handle = new WeakMap(), _AccountTrackerController_instances = new WeakSet(), _AccountTrackerController_getCurrentChainId = function _AccountTrackerController_getCurrentChainId() { ++_AccountTrackerController_refreshMutex = new WeakMap(), _AccountTrackerController_includeStakedAssets = new WeakMap(), _AccountTrackerController_getStakedBalanceForChain = new WeakMap(), _AccountTrackerController_handle = new WeakMap(), _AccountTrackerController_instances = new WeakSet(), _AccountTrackerController_getCurrentChainId = function _AccountTrackerController_getCurrentChainId() { + const { selectedNetworkClientId } = this.messagingSystem.call('NetworkController:getState'); + const { configuration: { chainId }, } = this.messagingSystem.call('NetworkController:getNetworkClientById', selectedNetworkClientId); + return chainId; +diff --git a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.cjs b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.cjs +index a8ba346..6b39491 100644 +--- a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.cjs ++++ b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.cjs +@@ -15,7 +15,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { + }; + var _AssetsContractController_instances, _AssetsContractController_provider, _AssetsContractController_ipfsGateway, _AssetsContractController_chainId, _AssetsContractController_registerActionHandlers, _AssetsContractController_registerEventSubscriptions, _AssetsContractController_getCorrectProvider, _AssetsContractController_getCorrectChainId; + Object.defineProperty(exports, "__esModule", { value: true }); +-exports.AssetsContractController = exports.MISSING_PROVIDER_ERROR = exports.SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID = void 0; ++exports.AssetsContractController = exports.MISSING_PROVIDER_ERROR = exports.STAKING_CONTRACT_ADDRESS_BY_CHAINID = exports.SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID = void 0; ++// import { BigNumber } from '@ethersproject/bignumber'; ++const bignumber_1 = require("@ethersproject/bignumber"); + const contracts_1 = require("@ethersproject/contracts"); + const providers_1 = require("@ethersproject/providers"); + const controller_utils_1 = require("@metamask/controller-utils"); +@@ -51,6 +53,10 @@ exports.SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID = { + [assetsUtil_1.SupportedTokenDetectionNetworks.moonbeam]: '0x6aa75276052d96696134252587894ef5ffa520af', + [assetsUtil_1.SupportedTokenDetectionNetworks.moonriver]: '0x6aa75276052d96696134252587894ef5ffa520af', + }; ++exports.STAKING_CONTRACT_ADDRESS_BY_CHAINID = { ++ [assetsUtil_1.SupportedStakedBalanceNetworks.mainnet]: '0x4fef9d741011476750a243ac70b9789a63dd47df', ++ [assetsUtil_1.SupportedStakedBalanceNetworks.holesky]: '0x37bf0883c27365cffcd0c4202918df930989891f', ++}; + exports.MISSING_PROVIDER_ERROR = 'AssetsContractController failed to set the provider correctly. A provider must be set for this method to be available'; + /** + * The name of the {@link AssetsContractController} +@@ -333,6 +339,60 @@ class AssetsContractController { + } + return nonZeroBalances; + } ++ /** ++ * Get the staked ethereum balance for an address in a single call. ++ * ++ * @param address - The address to check staked ethereum balance for. ++ * @param networkClientId - Network Client ID to fetch the provider with. ++ * @returns The hex staked ethereum balance for address. ++ */ ++ async getStakedBalanceForChain(address, networkClientId) { ++ const chainId = __classPrivateFieldGet(this, _AssetsContractController_instances, "m", _AssetsContractController_getCorrectChainId).call(this, networkClientId); ++ const provider = __classPrivateFieldGet(this, _AssetsContractController_instances, "m", _AssetsContractController_getCorrectProvider).call(this, networkClientId); ++ // balance defaults to zero ++ let balance = bignumber_1.BigNumber.from(0); ++ // Only fetch staked balance on supported networks ++ if (![ ++ assetsUtil_1.SupportedStakedBalanceNetworks.mainnet, ++ assetsUtil_1.SupportedStakedBalanceNetworks.holesky, ++ ].includes(chainId)) { ++ return undefined; ++ } ++ // Only fetch staked balance if contract address exists ++ if (!((id) => id in exports.STAKING_CONTRACT_ADDRESS_BY_CHAINID)(chainId)) { ++ return undefined; ++ } ++ const contractAddress = exports.STAKING_CONTRACT_ADDRESS_BY_CHAINID[chainId]; ++ const abi = [ ++ { ++ inputs: [{ internalType: 'address', name: 'account', type: 'address' }], ++ name: 'getShares', ++ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], ++ stateMutability: 'view', ++ type: 'function', ++ }, ++ { ++ inputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }], ++ name: 'convertToAssets', ++ outputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }], ++ stateMutability: 'view', ++ type: 'function', ++ }, ++ ]; ++ try { ++ const contract = new contracts_1.Contract(contractAddress, abi, provider); ++ const userShares = await contract.getShares(address); ++ // convert shares to assets only if address shares > 0 else return default balance ++ if (!userShares.lte(0)) { ++ balance = await contract.convertToAssets(userShares.toString()); ++ } ++ } ++ catch (error) { ++ // if we get an error, log and return the default value ++ console.error(error); ++ } ++ return balance.toHexString(); ++ } + } + exports.AssetsContractController = AssetsContractController; + _AssetsContractController_provider = new WeakMap(), _AssetsContractController_ipfsGateway = new WeakMap(), _AssetsContractController_chainId = new WeakMap(), _AssetsContractController_instances = new WeakSet(), _AssetsContractController_registerActionHandlers = function _AssetsContractController_registerActionHandlers() { +diff --git a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.d.cts b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.d.cts +index d7d9b61..b7bf69d 100644 +--- a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.d.cts ++++ b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.d.cts +@@ -32,6 +32,10 @@ export declare const SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID: { + readonly "0x504": "0x6aa75276052d96696134252587894ef5ffa520af"; + readonly "0x505": "0x6aa75276052d96696134252587894ef5ffa520af"; + }; ++export declare const STAKING_CONTRACT_ADDRESS_BY_CHAINID: { ++ readonly "0x1": "0x4fef9d741011476750a243ac70b9789a63dd47df"; ++ readonly "0x4268": "0x37bf0883c27365cffcd0c4202918df930989891f"; ++}; + export declare const MISSING_PROVIDER_ERROR = "AssetsContractController failed to set the provider correctly. A provider must be set for this method to be available"; + /** + * @type BalanceMap +@@ -98,6 +102,7 @@ export type AllowedEvents = PreferencesControllerStateChangeEvent | NetworkContr + * The messenger of the {@link AssetsContractController}. + */ + export type AssetsContractControllerMessenger = RestrictedControllerMessenger; ++export type StakedBalance = string | undefined; + /** + * Controller that interacts with contracts on mainnet through web3 + */ +@@ -272,6 +277,14 @@ export declare class AssetsContractController { + * @returns The list of non-zero token balances. + */ + getBalancesInSingleCall(selectedAddress: string, tokensToDetect: string[], networkClientId?: NetworkClientId): Promise; ++ /** ++ * Get the staked ethereum balance for an address in a single call. ++ * ++ * @param address - The address to check staked ethereum balance for. ++ * @param networkClientId - Network Client ID to fetch the provider with. ++ * @returns The hex staked ethereum balance for address. ++ */ ++ getStakedBalanceForChain(address: string, networkClientId?: NetworkClientId): Promise; + } + export default AssetsContractController; + //# sourceMappingURL=AssetsContractController.d.cts.map +\ No newline at end of file +diff --git a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.d.mts b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.d.mts +index a7916fb..9cbee8c 100644 +--- a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.d.mts ++++ b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.d.mts +@@ -32,6 +32,10 @@ export declare const SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID: { + readonly "0x504": "0x6aa75276052d96696134252587894ef5ffa520af"; + readonly "0x505": "0x6aa75276052d96696134252587894ef5ffa520af"; + }; ++export declare const STAKING_CONTRACT_ADDRESS_BY_CHAINID: { ++ readonly "0x1": "0x4fef9d741011476750a243ac70b9789a63dd47df"; ++ readonly "0x4268": "0x37bf0883c27365cffcd0c4202918df930989891f"; ++}; + export declare const MISSING_PROVIDER_ERROR = "AssetsContractController failed to set the provider correctly. A provider must be set for this method to be available"; + /** + * @type BalanceMap +@@ -98,6 +102,7 @@ export type AllowedEvents = PreferencesControllerStateChangeEvent | NetworkContr + * The messenger of the {@link AssetsContractController}. + */ + export type AssetsContractControllerMessenger = RestrictedControllerMessenger; ++export type StakedBalance = string | undefined; + /** + * Controller that interacts with contracts on mainnet through web3 + */ +@@ -272,6 +277,14 @@ export declare class AssetsContractController { + * @returns The list of non-zero token balances. + */ + getBalancesInSingleCall(selectedAddress: string, tokensToDetect: string[], networkClientId?: NetworkClientId): Promise; ++ /** ++ * Get the staked ethereum balance for an address in a single call. ++ * ++ * @param address - The address to check staked ethereum balance for. ++ * @param networkClientId - Network Client ID to fetch the provider with. ++ * @returns The hex staked ethereum balance for address. ++ */ ++ getStakedBalanceForChain(address: string, networkClientId?: NetworkClientId): Promise; + } + export default AssetsContractController; + //# sourceMappingURL=AssetsContractController.d.mts.map +\ No newline at end of file +diff --git a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.mjs b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.mjs +index 15a0e56..deeb1eb 100644 +--- a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.mjs ++++ b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.mjs +@@ -16,13 +16,15 @@ function $importDefault(module) { + } + return module; + } ++// import { BigNumber } from '@ethersproject/bignumber'; ++import { BigNumber } from "@ethersproject/bignumber"; + import { Contract } from "@ethersproject/contracts"; + import { Web3Provider } from "@ethersproject/providers"; + import { IPFS_DEFAULT_GATEWAY_URL } from "@metamask/controller-utils"; + import { getKnownPropertyNames } from "@metamask/utils"; + import $abiSingleCallBalancesContract from "single-call-balance-checker-abi"; + const abiSingleCallBalancesContract = $importDefault($abiSingleCallBalancesContract); +-import { SupportedTokenDetectionNetworks } from "./assetsUtil.mjs"; ++import { SupportedStakedBalanceNetworks, SupportedTokenDetectionNetworks } from "./assetsUtil.mjs"; + import { ERC20Standard } from "./Standards/ERC20Standard.mjs"; + import { ERC1155Standard } from "./Standards/NftStandards/ERC1155/ERC1155Standard.mjs"; + import { ERC721Standard } from "./Standards/NftStandards/ERC721/ERC721Standard.mjs"; +@@ -52,6 +54,10 @@ export const SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID = { + [SupportedTokenDetectionNetworks.moonbeam]: '0x6aa75276052d96696134252587894ef5ffa520af', + [SupportedTokenDetectionNetworks.moonriver]: '0x6aa75276052d96696134252587894ef5ffa520af', + }; ++export const STAKING_CONTRACT_ADDRESS_BY_CHAINID = { ++ [SupportedStakedBalanceNetworks.mainnet]: '0x4fef9d741011476750a243ac70b9789a63dd47df', ++ [SupportedStakedBalanceNetworks.holesky]: '0x37bf0883c27365cffcd0c4202918df930989891f', ++}; + export const MISSING_PROVIDER_ERROR = 'AssetsContractController failed to set the provider correctly. A provider must be set for this method to be available'; + /** + * The name of the {@link AssetsContractController} +@@ -334,6 +340,60 @@ export class AssetsContractController { + } + return nonZeroBalances; + } ++ /** ++ * Get the staked ethereum balance for an address in a single call. ++ * ++ * @param address - The address to check staked ethereum balance for. ++ * @param networkClientId - Network Client ID to fetch the provider with. ++ * @returns The hex staked ethereum balance for address. ++ */ ++ async getStakedBalanceForChain(address, networkClientId) { ++ const chainId = __classPrivateFieldGet(this, _AssetsContractController_instances, "m", _AssetsContractController_getCorrectChainId).call(this, networkClientId); ++ const provider = __classPrivateFieldGet(this, _AssetsContractController_instances, "m", _AssetsContractController_getCorrectProvider).call(this, networkClientId); ++ // balance defaults to zero ++ let balance = BigNumber.from(0); ++ // Only fetch staked balance on supported networks ++ if (![ ++ SupportedStakedBalanceNetworks.mainnet, ++ SupportedStakedBalanceNetworks.holesky, ++ ].includes(chainId)) { ++ return undefined; ++ } ++ // Only fetch staked balance if contract address exists ++ if (!((id) => id in STAKING_CONTRACT_ADDRESS_BY_CHAINID)(chainId)) { ++ return undefined; ++ } ++ const contractAddress = STAKING_CONTRACT_ADDRESS_BY_CHAINID[chainId]; ++ const abi = [ ++ { ++ inputs: [{ internalType: 'address', name: 'account', type: 'address' }], ++ name: 'getShares', ++ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], ++ stateMutability: 'view', ++ type: 'function', ++ }, ++ { ++ inputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }], ++ name: 'convertToAssets', ++ outputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }], ++ stateMutability: 'view', ++ type: 'function', ++ }, ++ ]; ++ try { ++ const contract = new Contract(contractAddress, abi, provider); ++ const userShares = await contract.getShares(address); ++ // convert shares to assets only if address shares > 0 else return default balance ++ if (!userShares.lte(0)) { ++ balance = await contract.convertToAssets(userShares.toString()); ++ } ++ } ++ catch (error) { ++ // if we get an error, log and return the default value ++ console.error(error); ++ } ++ return balance.toHexString(); ++ } + } + _AssetsContractController_provider = new WeakMap(), _AssetsContractController_ipfsGateway = new WeakMap(), _AssetsContractController_chainId = new WeakMap(), _AssetsContractController_instances = new WeakSet(), _AssetsContractController_registerActionHandlers = function _AssetsContractController_registerActionHandlers() { + const methodsExcludedFromMessenger = [ diff --git a/node_modules/@metamask/assets-controllers/dist/NftController.cjs b/node_modules/@metamask/assets-controllers/dist/NftController.cjs index 82613cf..f7fd41e 100644 --- a/node_modules/@metamask/assets-controllers/dist/NftController.cjs @@ -377,3 +988,94 @@ index 3fbbc66..4f99591 100644 export default TokensController; //# sourceMappingURL=TokensController.d.cts.map \ No newline at end of file +diff --git a/node_modules/@metamask/assets-controllers/dist/assetsUtil.cjs b/node_modules/@metamask/assets-controllers/dist/assetsUtil.cjs +index e90a1b6..c2e83cf 100644 +--- a/node_modules/@metamask/assets-controllers/dist/assetsUtil.cjs ++++ b/node_modules/@metamask/assets-controllers/dist/assetsUtil.cjs +@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); +-exports.fetchTokenContractExchangeRates = exports.reduceInBatchesSerially = exports.divideIntoBatches = exports.ethersBigNumberToBN = exports.addUrlProtocolPrefix = exports.getFormattedIpfsUrl = exports.getIpfsCIDv1AndPath = exports.removeIpfsProtocolPrefix = exports.isTokenListSupportedForNetwork = exports.isTokenDetectionSupportedForNetwork = exports.SupportedTokenDetectionNetworks = exports.formatIconUrlWithProxy = exports.formatAggregatorNames = exports.hasNewCollectionFields = exports.compareNftMetadata = exports.TOKEN_PRICES_BATCH_SIZE = void 0; ++exports.fetchTokenContractExchangeRates = exports.reduceInBatchesSerially = exports.divideIntoBatches = exports.ethersBigNumberToBN = exports.addUrlProtocolPrefix = exports.getFormattedIpfsUrl = exports.getIpfsCIDv1AndPath = exports.removeIpfsProtocolPrefix = exports.isTokenListSupportedForNetwork = exports.isTokenDetectionSupportedForNetwork = exports.SupportedStakedBalanceNetworks = exports.SupportedTokenDetectionNetworks = exports.formatIconUrlWithProxy = exports.formatAggregatorNames = exports.hasNewCollectionFields = exports.compareNftMetadata = exports.TOKEN_PRICES_BATCH_SIZE = void 0; + const controller_utils_1 = require("@metamask/controller-utils"); + const utils_1 = require("@metamask/utils"); + const bn_js_1 = __importDefault(require("bn.js")); +@@ -168,6 +168,18 @@ var SupportedTokenDetectionNetworks; + // eslint-disable-next-line @typescript-eslint/naming-convention + SupportedTokenDetectionNetworks["moonriver"] = "0x505"; + })(SupportedTokenDetectionNetworks || (exports.SupportedTokenDetectionNetworks = SupportedTokenDetectionNetworks = {})); ++/** ++ * Networks where staked balance is supported - Values are in hex format ++ */ ++var SupportedStakedBalanceNetworks; ++(function (SupportedStakedBalanceNetworks) { ++ // TODO: Either fix this lint violation or explain why it's necessary to ignore. ++ // eslint-disable-next-line @typescript-eslint/naming-convention ++ SupportedStakedBalanceNetworks["mainnet"] = "0x1"; ++ // TODO: Either fix this lint violation or explain why it's necessary to ignore. ++ // eslint-disable-next-line @typescript-eslint/naming-convention ++ SupportedStakedBalanceNetworks["holesky"] = "0x4268"; ++})(SupportedStakedBalanceNetworks || (exports.SupportedStakedBalanceNetworks = SupportedStakedBalanceNetworks = {})); + /** + * Check if token detection is enabled for certain networks. + * +diff --git a/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.cts b/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.cts +index 4007a3b..cca8a6b 100644 +--- a/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.cts ++++ b/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.cts +@@ -68,6 +68,13 @@ export declare enum SupportedTokenDetectionNetworks { + moonbeam = "0x504", + moonriver = "0x505" + } ++/** ++ * Networks where staked balance is supported - Values are in hex format ++ */ ++export declare enum SupportedStakedBalanceNetworks { ++ mainnet = "0x1", ++ holesky = "0x4268" ++} + /** + * Check if token detection is enabled for certain networks. + * +diff --git a/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.mts b/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.mts +index e4d7d0a..0674deb 100644 +--- a/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.mts ++++ b/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.mts +@@ -68,6 +68,13 @@ export declare enum SupportedTokenDetectionNetworks { + moonbeam = "0x504", + moonriver = "0x505" + } ++/** ++ * Networks where staked balance is supported - Values are in hex format ++ */ ++export declare enum SupportedStakedBalanceNetworks { ++ mainnet = "0x1", ++ holesky = "0x4268" ++} + /** + * Check if token detection is enabled for certain networks. + * +diff --git a/node_modules/@metamask/assets-controllers/dist/assetsUtil.mjs b/node_modules/@metamask/assets-controllers/dist/assetsUtil.mjs +index a53a5f0..1ae8b32 100644 +--- a/node_modules/@metamask/assets-controllers/dist/assetsUtil.mjs ++++ b/node_modules/@metamask/assets-controllers/dist/assetsUtil.mjs +@@ -165,6 +165,18 @@ export var SupportedTokenDetectionNetworks; + // eslint-disable-next-line @typescript-eslint/naming-convention + SupportedTokenDetectionNetworks["moonriver"] = "0x505"; + })(SupportedTokenDetectionNetworks || (SupportedTokenDetectionNetworks = {})); ++/** ++ * Networks where staked balance is supported - Values are in hex format ++ */ ++export var SupportedStakedBalanceNetworks; ++(function (SupportedStakedBalanceNetworks) { ++ // TODO: Either fix this lint violation or explain why it's necessary to ignore. ++ // eslint-disable-next-line @typescript-eslint/naming-convention ++ SupportedStakedBalanceNetworks["mainnet"] = "0x1"; ++ // TODO: Either fix this lint violation or explain why it's necessary to ignore. ++ // eslint-disable-next-line @typescript-eslint/naming-convention ++ SupportedStakedBalanceNetworks["holesky"] = "0x4268"; ++})(SupportedStakedBalanceNetworks || (SupportedStakedBalanceNetworks = {})); + /** + * Check if token detection is enabled for certain networks. + *