Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Support for Solana Devnet ([#31702](https://github.com/MetaMask/metamask-extension/pull/31702))
- [Beta] Create Solana account automatically on wallet creation or SRP import [#32038](https://github.com/MetaMask/metamask-extension/pull/32038)

## [12.15.2]
### Added
Expand Down
44 changes: 44 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ import { BRIDGE_API_BASE_URL } from '../../shared/constants/bridge';
import { BridgeStatusAction } from '../../shared/types/bridge-status';
///: BEGIN:ONLY_INCLUDE_IF(solana)
import { addDiscoveredSolanaAccounts } from '../../shared/lib/accounts';
import { SOLANA_WALLET_SNAP_ID } from '../../shared/lib/accounts/solana-wallet-snap';
///: END:ONLY_INCLUDE_IF
import {
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
Expand Down Expand Up @@ -1264,6 +1265,9 @@ export default class MetamaskController extends EventEmitter {
if (!prevCompletedOnboarding && currCompletedOnboarding) {
const { address } = this.accountsController.getSelectedAccount();

///: BEGIN:ONLY_INCLUDE_IF(solana)
await this._addSolanaAccount();
///: END:ONLY_INCLUDE_IF
await this._addAccountsWithBalance();

this.postOnboardingInitialization();
Expand Down Expand Up @@ -4726,6 +4730,9 @@ export default class MetamaskController extends EventEmitter {
this.accountsController.getAccountByAddress(newAccountAddress);
this.accountsController.setSelectedAccount(account.id);

///: BEGIN:ONLY_INCLUDE_IF(solana)
await this._addSolanaAccount(id);
///: END:ONLY_INCLUDE_IF
await this._addAccountsWithBalance(id);

return newAccountAddress;
Expand Down Expand Up @@ -4808,6 +4815,9 @@ export default class MetamaskController extends EventEmitter {
);

if (completedOnboarding) {
///: BEGIN:ONLY_INCLUDE_IF(solana)
await this._addSolanaAccount();
///: END:ONLY_INCLUDE_IF
await this._addAccountsWithBalance();

// This must be set as soon as possible to communicate to the
Expand Down Expand Up @@ -4910,6 +4920,40 @@ export default class MetamaskController extends EventEmitter {
}
}

/**
* Adds Solana account to the keyring.
*
* @param {string} keyringId - The ID of the keyring to add the account to.
*/
///: BEGIN:ONLY_INCLUDE_IF(solana)
async _addSolanaAccount(keyringId) {
const snapId = SOLANA_WALLET_SNAP_ID;
let entropySource = keyringId;
if (!entropySource) {
// Get the entropy source from the first HD keyring
const id = await this.keyringController.withKeyring(
{ type: KeyringTypes.hd },
async ({ metadata }) => {
return metadata.id;
},
);
entropySource = id;
}

const keyring = await this.getSnapKeyring();

return await keyring.createAccount(
snapId,
{ entropySource },
{
displayConfirmation: false,
displayAccountNameSuggestion: false,
setSelectedAccount: false,
},
);
}
///: END:ONLY_INCLUDE_IF

/**
* Encodes a BIP-39 mnemonic as the indices of words in the English BIP-39 wordlist.
*
Expand Down
80 changes: 73 additions & 7 deletions app/scripts/metamask-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,40 @@ describe('MetaMaskController', () => {
allDetectedTokens: { '0x1': { [TEST_ADDRESS_2]: [{}] } },
});

const mockSnapKeyring = {
createAccount: jest
.fn()
.mockResolvedValue({ address: 'mockedAddress' }),
};

const originalGetKeyringsByType =
metamaskController.keyringController.getKeyringsByType;
let snapKeyringCallCount = 0;
jest
.spyOn(metamaskController.keyringController, 'getKeyringsByType')
.mockImplementation((type) => {
if (type === 'Snap Keyring') {
snapKeyringCallCount += 1;

if (snapKeyringCallCount === 1) {
// First call - use original implementation to let controller initialize snap keyring
return originalGetKeyringsByType.call(
metamaskController.keyringController,
type,
);
}
// Second call and beyond - return mock
console.log('returning mocked snap keyring!');
return [mockSnapKeyring];
}

// For other types, always use original implementation
return originalGetKeyringsByType.call(
metamaskController.keyringController,
type,
);
});

await metamaskController.createNewVaultAndRestore(
'foobar1337',
TEST_SEED,
Expand Down Expand Up @@ -3912,6 +3946,39 @@ describe('MetaMaskController', () => {
it('generates a new hd keyring instance with a mnemonic', async () => {
const password = 'what-what-what';
jest.spyOn(metamaskController, 'getBalance').mockResolvedValue('0x0');
const mockSnapKeyring = {
createAccount: jest
.fn()
.mockResolvedValue({ address: 'mockedAddress' }),
};

const originalGetKeyringsByType =
metamaskController.keyringController.getKeyringsByType;
let snapKeyringCallCount = 0;
jest
.spyOn(metamaskController.keyringController, 'getKeyringsByType')
.mockImplementation((type) => {
if (type === 'Snap Keyring') {
snapKeyringCallCount += 1;

if (snapKeyringCallCount === 1) {
// First call - use original implementation to let controller initialize snap keyring
return originalGetKeyringsByType.call(
metamaskController.keyringController,
type,
);
}
// Second call and beyond - return mock
console.log('returning mocked snap keyring!');
return [mockSnapKeyring];
}

// For other types, always use original implementation
return originalGetKeyringsByType.call(
metamaskController.keyringController,
type,
);
});

await metamaskController.createNewVaultAndRestore(password, TEST_SEED);

Expand Down Expand Up @@ -3971,10 +4038,9 @@ describe('MetaMaskController', () => {
.mockImplementation(mockDiscoverAccounts);

const mockCreateAccount = jest.fn().mockResolvedValue(undefined);
const mockSnapKeyring = { createAccount: mockCreateAccount };
jest
.spyOn(metamaskController, 'getSnapKeyring')
.mockResolvedValue(mockSnapKeyring);
.mockResolvedValue({ createAccount: mockCreateAccount });

await metamaskController.createNewVaultAndRestore(password, TEST_SEED);
await metamaskController.importMnemonicToVault(TEST_SEED_ALT);
Expand All @@ -4000,14 +4066,14 @@ describe('MetaMaskController', () => {
expect(mockDiscoverAccounts.mock.calls[2][2]).toBe(2);

// Assert that createAccount was called correctly for each discovered account
expect(mockCreateAccount).toHaveBeenCalledTimes(2);
expect(mockCreateAccount).toHaveBeenCalledTimes(3);

// All calls should use the solana snap ID
expect(mockCreateAccount.mock.calls[0][0]).toStrictEqual(
expect.stringContaining('solana-wallet'),
);
// First call should use derivation path on index 0
expect(mockCreateAccount.mock.calls[0][1]).toStrictEqual({
// Second call should use derivation path on index 0
expect(mockCreateAccount.mock.calls[1][1]).toStrictEqual({
derivationPath: "m/44'/501'/0'/0'",
entropySource: expect.any(String),
});
Expand All @@ -4018,8 +4084,8 @@ describe('MetaMaskController', () => {
setSelectedAccount: false,
});

// Second call should use derivation path on index 1
expect(mockCreateAccount.mock.calls[1][1]).toStrictEqual({
// Third call should use derivation path on index 1
expect(mockCreateAccount.mock.calls[2][1]).toStrictEqual({
derivationPath: "m/44'/501'/1'/0'",
entropySource: expect.any(String),
});
Expand Down
Loading