Skip to content

Commit

Permalink
chore: adds Solana support for the account overview (#28411)
Browse files Browse the repository at this point in the history
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

We added support for the Solana account overview. Now when we select a
Solana address the user will be able to see its details in the home
view.
Also since the overview is the same for SOL and BTC, in order to not
repeat components, we've renamed as "non-evm" the existing BTC ones, and
reused them.

![Screenshot 2024-11-13 at 13 53
42](https://github.com/user-attachments/assets/649c1b1c-2da2-4f12-a18d-3549d8739c0e)

## **Related issues**

Fixes:

## **Manual testing steps**

As of right now, manually testing is a bit complex, it needs to run the
snap manually and the extension, since we 1st need to publish a new
release to npm with more up to date work. The snap version we have in
npm is outdated and won't support this flow. That said, if you want to
go ahead and run locally the steps are the following:

1. Clone the [ Solana Snap
monorepo](https://github.com/MetaMask/snap-solana-wallet) and run it
locally with `yarn` and then `yarn start`
2. In the extension, at this branch, apply the following changes and run
the extension as flask:
```
At builds.yml add the solana feature to the flask build:
 
     features:
       - build-flask
       - keyring-snaps
+      - solana

At shared/lib/accounts/solana-wallet-snap.ts point the snap ID to the snap localhost:

-export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId;
+//export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId;
+export const SOLANA_WALLET_SNAP_ID: SnapId = "local:http://localhost:8080/";
```
3. Manually install the snap via the snap dapp at http://localhost:3000
4. Enable the Solana account via Settings > Experimental > Enable Solana
account
5. Create a Solana account from the account-list menu and see the
account overview of it

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.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.

---------

Co-authored-by: MetaMask Bot <[email protected]>
Co-authored-by: Charly Chevalier <[email protected]>
  • Loading branch information
3 people authored Nov 26, 2024
1 parent 6dda444 commit be2b439
Show file tree
Hide file tree
Showing 23 changed files with 243 additions and 93 deletions.
3 changes: 3 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions app/scripts/lib/accounts/BalancesController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
InternalAccount,
} from '@metamask/keyring-api';
import { createMockInternalAccount } from '../../../../test/jest/mocks';
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';
import {
BalancesController,
AllowedActions,
Expand All @@ -25,6 +26,9 @@ const mockBtcAccount = createMockInternalAccount({
name: 'mock-btc-snap',
enabled: true,
},
options: {
scope: MultichainNetworks.BITCOIN_TESTNET,
},
});

const mockBalanceResult = {
Expand Down
79 changes: 67 additions & 12 deletions app/scripts/lib/accounts/BalancesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type CaipAssetType,
type InternalAccount,
isEvmAccountType,
SolAccountType,
} from '@metamask/keyring-api';
import type { HandleSnapRequest } from '@metamask/snaps-controllers';
import type { SnapId } from '@metamask/snaps-sdk';
Expand All @@ -23,6 +24,8 @@ import type {
AccountsControllerAccountRemovedEvent,
AccountsControllerListMultichainAccountsAction,
} from '@metamask/accounts-controller';
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';
import { MULTICHAIN_NETWORK_TO_ASSET_TYPES } from '../../../../shared/constants/multichain/assets';
import { isBtcMainnetAddress } from '../../../../shared/lib/multichain';
import { BalancesTracker } from './BalancesTracker';

Expand Down Expand Up @@ -122,13 +125,17 @@ const balancesControllerMetadata = {
},
};

const BTC_TESTNET_ASSETS = ['bip122:000000000933ea01ad0ee984209779ba/slip44:0'];
const BTC_MAINNET_ASSETS = ['bip122:000000000019d6689c085ae165831e93/slip44:0'];
const BTC_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds
const SOLANA_AVG_BLOCK_TIME = 400; // 400 milliseconds

// NOTE: We set an interval of half the average block time to mitigate when our interval
// is de-synchronized with the actual block time.
export const BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2;
export const BTC_BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2;

const BALANCE_CHECK_INTERVALS = {
[BtcAccountType.P2wpkh]: BTC_BALANCES_UPDATE_TIME,
[SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME,
};

/**
* The BalancesController is responsible for fetching and caching account
Expand Down Expand Up @@ -165,7 +172,7 @@ export class BalancesController extends BaseController<
// Register all non-EVM accounts into the tracker
for (const account of this.#listAccounts()) {
if (this.#isNonEvmAccount(account)) {
this.#tracker.track(account.id, BALANCES_UPDATE_TIME);
this.#tracker.track(account.id, this.#getBlockTimeFor(account));
}
}

Expand Down Expand Up @@ -193,6 +200,23 @@ export class BalancesController extends BaseController<
this.#tracker.stop();
}

/**
* Gets the block time for a given account.
*
* @param account - The account to get the block time for.
* @returns The block time for the account.
*/
#getBlockTimeFor(account: InternalAccount): number {
if (account.type in BALANCE_CHECK_INTERVALS) {
return BALANCE_CHECK_INTERVALS[
account.type as keyof typeof BALANCE_CHECK_INTERVALS
];
}
throw new Error(
`Unsupported account type for balance tracking: ${account.type}`,
);
}

/**
* Lists the multichain accounts coming from the `AccountsController`.
*
Expand All @@ -207,15 +231,16 @@ export class BalancesController extends BaseController<
/**
* Lists the accounts that we should get balances for.
*
* Currently, we only get balances for P2WPKH accounts, but this will change
* in the future when we start support other non-EVM account types.
*
* @returns A list of accounts that we should get balances for.
*/
#listAccounts(): InternalAccount[] {
const accounts = this.#listMultichainAccounts();

return accounts.filter((account) => account.type === BtcAccountType.P2wpkh);
return accounts.filter(
(account) =>
account.type === SolAccountType.DataAccount ||
account.type === BtcAccountType.P2wpkh,
);
}

/**
Expand Down Expand Up @@ -249,12 +274,13 @@ export class BalancesController extends BaseController<
const partialState: BalancesControllerState = { balances: {} };

if (account.metadata.snap) {
const scope = this.#getScopeFrom(account);
const assetTypes = MULTICHAIN_NETWORK_TO_ASSET_TYPES[scope];

partialState.balances[account.id] = await this.#getBalances(
account.id,
account.metadata.snap.id,
isBtcMainnetAddress(account.address)
? BTC_MAINNET_ASSETS
: BTC_TESTNET_ASSETS,
assetTypes,
);
}

Expand Down Expand Up @@ -312,7 +338,7 @@ export class BalancesController extends BaseController<
return;
}

this.#tracker.track(account.id, BTC_AVG_BLOCK_TIME);
this.#tracker.track(account.id, this.#getBlockTimeFor(account));
// NOTE: Unfortunately, we cannot update the balance right away here, because
// messenger's events are running synchronously and fetching the balance is
// asynchronous.
Expand Down Expand Up @@ -376,4 +402,33 @@ export class BalancesController extends BaseController<
})) as Promise<Json>,
});
}

/**
* Gets the network scope for a given account.
*
* @param account - The account to get the scope for.
* @returns The network scope for the account.
* @throws If the account type is unknown or unsupported.
*/
#getScopeFrom(account: InternalAccount): MultichainNetworks {
// TODO: Use the new `account.scopes` once available in the `keyring-api`.

// For Bitcoin accounts, we get the scope based on the address format.
if (account.type === BtcAccountType.P2wpkh) {
if (isBtcMainnetAddress(account.address)) {
return MultichainNetworks.BITCOIN;
}
return MultichainNetworks.BITCOIN_TESTNET;
}

// For Solana accounts, we know we have a `scope` on the account's `options` bag.
if (account.type === SolAccountType.DataAccount) {
if (!account.options.scope) {
throw new Error('Solana account scope is undefined');
}
return account.options.scope as MultichainNetworks;
}

throw new Error(`Unsupported non-EVM account type: ${account.type}`);
}
}
2 changes: 1 addition & 1 deletion app/scripts/metamask-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { createMockInternalAccount } from '../../test/jest/mocks';
import { mockNetworkState } from '../../test/stub/networks';
import {
BalancesController as MultichainBalancesController,
BALANCES_UPDATE_TIME as MULTICHAIN_BALANCES_UPDATE_TIME,
BTC_BALANCES_UPDATE_TIME as MULTICHAIN_BALANCES_UPDATE_TIME,
} from './lib/accounts/BalancesController';
import { BalancesTracker as MultichainBalancesTracker } from './lib/accounts/BalancesTracker';
import { deferredPromise } from './lib/util';
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,8 @@
"resolve-url-loader>es6-iterator>d>es5-ext": false,
"resolve-url-loader>es6-iterator>d>es5-ext>esniff>es5-ext": false,
"level>classic-level": false,
"jest-preview": false
"jest-preview": false,
"@metamask/solana-wallet-snap>@solana/web3.js>bigint-buffer": false
}
},
"packageManager": "[email protected]"
Expand Down
18 changes: 18 additions & 0 deletions shared/constants/multichain/assets.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CaipAssetType } from '@metamask/keyring-api';
import { MultichainNetworks } from './networks';

export const MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 = {
Expand All @@ -13,3 +14,20 @@ export enum MultichainNativeAssets {
SOLANA_DEVNET = `${MultichainNetworks.SOLANA_DEVNET}/slip44:501`,
SOLANA_TESTNET = `${MultichainNetworks.SOLANA_TESTNET}/slip44:501`,
}

/**
* Maps network identifiers to their corresponding native asset types.
* Each network is mapped to an array containing its native asset for consistency.
*/
export const MULTICHAIN_NETWORK_TO_ASSET_TYPES: Record<
MultichainNetworks,
CaipAssetType[]
> = {
[MultichainNetworks.SOLANA]: [MultichainNativeAssets.SOLANA],
[MultichainNetworks.SOLANA_TESTNET]: [MultichainNativeAssets.SOLANA_TESTNET],
[MultichainNetworks.SOLANA_DEVNET]: [MultichainNativeAssets.SOLANA_DEVNET],
[MultichainNetworks.BITCOIN]: [MultichainNativeAssets.BITCOIN],
[MultichainNetworks.BITCOIN_TESTNET]: [
MultichainNativeAssets.BITCOIN_TESTNET,
],
};
6 changes: 6 additions & 0 deletions shared/constants/multichain/networks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CaipChainId } from '@metamask/utils';
import { BtcAccountType, SolAccountType } from '@metamask/keyring-api';
import {
isBtcMainnetAddress,
isBtcTestnetAddress,
Expand Down Expand Up @@ -33,6 +34,11 @@ export enum MultichainNetworks {
SOLANA_TESTNET = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z',
}

export const MULTICHAIN_ACCOUNT_TYPE_TO_MAINNET = {
[BtcAccountType.P2wpkh]: MultichainNetworks.BITCOIN,
[SolAccountType.DataAccount]: MultichainNetworks.SOLANA,
} as const;

export const BITCOIN_TOKEN_IMAGE_URL = './images/bitcoin-logo.svg';
export const SOLANA_TOKEN_IMAGE_URL = './images/solana-logo.svg';

Expand Down
8 changes: 7 additions & 1 deletion shared/constants/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ export const CURRENCY_SYMBOLS = {
AVALANCHE: 'AVAX',
BNB: 'BNB',
BUSD: 'BUSD',
BTC: 'BTC', // Do we wanna mix EVM and non-EVM here?
CELO: 'CELO',
DAI: 'DAI',
GNOSIS: 'XDAI',
Expand All @@ -322,8 +321,15 @@ export const CURRENCY_SYMBOLS = {
ONE: 'ONE',
} as const;

// Non-EVM currency symbols
export const NON_EVM_CURRENCY_SYMBOLS = {
BTC: 'BTC',
SOL: 'SOL',
} as const;

const CHAINLIST_CURRENCY_SYMBOLS_MAP = {
...CURRENCY_SYMBOLS,
...NON_EVM_CURRENCY_SYMBOLS,
BASE: 'ETH',
LINEA_MAINNET: 'ETH',
OPBNB: 'BNB',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,11 @@
"petnamesEnabled": true,
"showMultiRpcModal": "boolean",
"isRedesignedConfirmationsDeveloperEnabled": "boolean",
"redesignedConfirmationsEnabled": true,
"redesignedTransactionsEnabled": "boolean",
"tokenSortConfig": "object",
"tokenNetworkFilter": {
"0x539": "boolean"
},
"shouldShowAggregatedBalancePopover": "boolean"
"shouldShowAggregatedBalancePopover": "boolean",
"tokenNetworkFilter": { "0x539": "boolean" },
"redesignedConfirmationsEnabled": true,
"redesignedTransactionsEnabled": "boolean"
},
"firstTimeFlowType": "import",
"completedOnboarding": true,
Expand Down Expand Up @@ -176,17 +174,15 @@
"gasEstimateType": "none",
"nonRPCGasFeeApisDisabled": "boolean",
"tokenList": "object",
"tokensChainsCache": {
"0x539": "object"
},
"tokenBalances": "object",
"tokensChainsCache": { "0x539": "object" },
"preventPollingOnNetworkRestart": false,
"tokens": "object",
"ignoredTokens": "object",
"detectedTokens": "object",
"allTokens": {},
"allIgnoredTokens": {},
"allDetectedTokens": {},
"tokenBalances": "object",
"smartTransactionsState": {
"fees": {},
"feesByChainId": "object",
Expand Down
5 changes: 4 additions & 1 deletion test/jest/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { KeyringTypes } from '@metamask/keyring-controller';
import { v4 as uuidv4 } from 'uuid';
import { keyringTypeToName } from '@metamask/accounts-controller';
import { Json } from '@metamask/utils';
import {
DraftTransaction,
draftTransactionInitialState,
Expand Down Expand Up @@ -186,6 +187,7 @@ export function createMockInternalAccount({
keyringType = KeyringTypes.hd,
lastSelected = 0,
snapOptions = undefined,
options = undefined,
}: {
name?: string;
address?: string;
Expand All @@ -197,6 +199,7 @@ export function createMockInternalAccount({
name: string;
id: string;
};
options?: Record<string, Json>;
} = {}) {
let methods;

Expand Down Expand Up @@ -236,7 +239,7 @@ export function createMockInternalAccount({
snap: snapOptions,
lastSelected,
},
options: {},
options: options ?? {},
methods,
type,
};
Expand Down
2 changes: 1 addition & 1 deletion ui/components/app/wallet-overview/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as EthOverview } from './eth-overview';
export { default as BtcOverview } from './btc-overview';
export { default as NonEvmOverview } from './non-evm-overview';
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import BtcOverview from './btc-overview';
import NonEvmOverview from './non-evm-overview';

export default {
title: 'Components/App/WalletOverview/BtcOverview',
component: BtcOverview,
component: NonEvmOverview,
parameters: {
docs: {
description: {
Expand All @@ -14,6 +14,6 @@ export default {
},
};

const Template = (args) => <BtcOverview {...args} />;
const Template = (args) => <NonEvmOverview {...args} />;

export const Default = Template.bind({});
Loading

0 comments on commit be2b439

Please sign in to comment.