Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update transaction controllers to use selected account #4244

Merged
Show file tree
Hide file tree
Changes from 4 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 packages/transaction-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@metamask/controller-utils": "^11.0.0",
"@metamask/eth-query": "^4.0.0",
"@metamask/gas-fee-controller": "^17.0.0",
"@metamask/keyring-api": "6.4.0",
Copy link
Contributor

@legobeat legobeat Jun 4, 2024

Choose a reason for hiding this comment

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

Noting that this is a type-only import and not under peerDependencies, this is a something that could be moved to devDependencies?

(If strict compatibility with the imported class is required, that indicates to me that it could also be a candidate for peerDependencies, if introducing that requirement here is indeed intended)

Copy link
Contributor

@mcmire mcmire Jun 5, 2024

Choose a reason for hiding this comment

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

Looking at the emitted type declaration file, it looks like the @metamask/keyring-api shows up there, so I believe this needs to be in dependencies, not devDependencies or peerDependencies so that it doesn't create a TypeScript error on build. Right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated, it looks like this is missing a ^:

Suggested change
"@metamask/keyring-api": "6.4.0",
"@metamask/keyring-api": "^6.4.0",

Copy link
Contributor

@legobeat legobeat Jun 6, 2024

Choose a reason for hiding this comment

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

@mcmire I may be missing something but it seems to me that if we just consider the peerDependencies aspect in isolation, it should also be here.
And if the peerDependencies is satisifed, then there should be no TypeScript error on build, and therefore no need for dependencies since the types will be provided by the peerDependencies entry?

Copy link
Contributor

@legobeat legobeat Jun 6, 2024

Choose a reason for hiding this comment

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

I'm thinking if it's not added in peerDependencies, using code may run the risk of getting inconsistent type-checking results or plain incompatibilities, due to using the wrong version of @metamask/keyring-api for its type-checking?

Specifically here, it would be wrt the getSelectedAccount constructor option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed the callback and used the messaging system instead to get the selected account. @metamask/keyring-api can just be a dev dependency for the tests.

"@metamask/metamask-eth-abis": "^3.1.1",
"@metamask/network-controller": "^19.0.0",
"@metamask/nonce-tracker": "^5.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '@metamask/controller-utils';
import EthQuery from '@metamask/eth-query';
import HttpProvider from '@metamask/ethjs-provider-http';
import { EthAccountType } from '@metamask/keyring-api';
import type {
BlockTracker,
NetworkController,
Expand Down Expand Up @@ -434,6 +435,20 @@ const MOCK_CUSTOM_NETWORK: MockNetwork = {
};

const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207';
const INTERNAL_ACCOUNT_MOCK = {
id: '58def058-d35f-49a1-a7ab-e2580565f6f5',
address: ACCOUNT_MOCK,
type: EthAccountType.Eoa,
options: {},
methods: [],
metadata: {
name: 'Account 1',
keyring: { type: 'HD Key Tree' },
importTime: 1631619180000,
lastSelected: 1631619180000,
},
};

const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211';
const NONCE_MOCK = 12;
const ACTION_ID_MOCK = '123456';
Expand Down Expand Up @@ -582,7 +597,7 @@ describe('TransactionController', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getNetworkClientRegistry: () => ({} as any),
getPermittedAccounts: async () => [ACCOUNT_MOCK],
getSelectedAddress: () => ACCOUNT_MOCK,
getSelectedAccount: () => INTERNAL_ACCOUNT_MOCK,
isMultichainEnabled: false,
hooks: {},
onNetworkStateChange: network.subscribe,
Expand Down
15 changes: 8 additions & 7 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
FetchGasFeeEstimateOptions,
GasFeeState,
} from '@metamask/gas-fee-controller';
import type { InternalAccount } from '@metamask/keyring-api';
import type {
BlockTracker,
NetworkClientId,
Expand Down Expand Up @@ -297,7 +298,7 @@ export type TransactionControllerOptions = {
getNetworkState: () => NetworkState;
getPermittedAccounts: (origin?: string) => Promise<string[]>;
getSavedGasFees?: (chainId: Hex) => SavedGasFees | undefined;
getSelectedAddress: () => string;
getSelectedAccount: () => InternalAccount;
incomingTransactions?: IncomingTransactionOptions;
isMultichainEnabled: boolean;
isSimulationEnabled?: () => boolean;
Expand Down Expand Up @@ -614,7 +615,7 @@ export class TransactionController extends BaseController<

private readonly getPermittedAccounts: (origin?: string) => Promise<string[]>;

private readonly getSelectedAddress: () => string;
private readonly getSelectedAccount: () => InternalAccount;

private readonly getExternalPendingTransactions: (
address: string,
Expand Down Expand Up @@ -733,7 +734,7 @@ export class TransactionController extends BaseController<
* @param options.getNetworkState - Gets the state of the network controller.
* @param options.getPermittedAccounts - Get accounts that a given origin has permissions for.
* @param options.getSavedGasFees - Gets the saved gas fee config.
* @param options.getSelectedAddress - Gets the address of the currently selected account.
* @param options.getSelectedAccount - Gets the currently selected account.
* @param options.incomingTransactions - Configuration options for incoming transaction support.
* @param options.isMultichainEnabled - Enable multichain support.
* @param options.isSimulationEnabled - Whether new transactions will be automatically simulated.
Expand Down Expand Up @@ -761,7 +762,7 @@ export class TransactionController extends BaseController<
getNetworkState,
getPermittedAccounts,
getSavedGasFees,
getSelectedAddress,
getSelectedAccount,
incomingTransactions = {},
isMultichainEnabled = false,
isSimulationEnabled,
Expand Down Expand Up @@ -802,7 +803,7 @@ export class TransactionController extends BaseController<
this.getGasFeeEstimates =
getGasFeeEstimates || (() => Promise.resolve({} as GasFeeState));
this.getPermittedAccounts = getPermittedAccounts;
this.getSelectedAddress = getSelectedAddress;
this.getSelectedAccount = getSelectedAccount;
this.getExternalPendingTransactions =
getExternalPendingTransactions ?? (() => []);
this.securityProviderRequest = securityProviderRequest;
Expand Down Expand Up @@ -1035,7 +1036,7 @@ export class TransactionController extends BaseController<
if (origin) {
await validateTransactionOrigin(
await this.getPermittedAccounts(origin),
this.getSelectedAddress(),
this.getSelectedAccount().address,
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this controller takes a function that allows for getting the currently selected account. However, this is an old pattern and we are slowly trying to move all controllers to use the messenger pattern for intercommunication.

Since you're making a breaking change anyway, what are your thoughts on extending the TransactionControllerMessenger to allow the AccountsController:getSelectedAccount action? You can do this by importing AccountsControllerGetSelectedAccountAction from @metamask/accounts-controller and adding it to AllowedActions. Then you can remove the getSelectedAccount option from the constructor altogether and replace this line with:

Suggested change
this.getSelectedAccount().address,
this.messagingSystem.call('AccountsController:getSelectedAccount').address,

txParams.from,
origin,
);
Expand Down Expand Up @@ -3430,7 +3431,7 @@ export class TransactionController extends BaseController<
}): IncomingTransactionHelper {
const incomingTransactionHelper = new IncomingTransactionHelper({
blockTracker,
getCurrentAccount: this.getSelectedAddress,
getCurrentAccount: this.getSelectedAccount,
getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers,
getChainId: chainId ? () => chainId : this.getChainId.bind(this),
isEnabled: this.#incomingTransactionOptions.isEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
InfuraNetworkType,
NetworkType,
} from '@metamask/controller-utils';
import type { InternalAccount } from '@metamask/keyring-api';
import { EthAccountType, EthMethod } from '@metamask/keyring-api';
import {
NetworkController,
NetworkClientType,
Expand All @@ -25,6 +27,7 @@ import assert from 'assert';
import nock from 'nock';
import type { SinonFakeTimers } from 'sinon';
import { useFakeTimers } from 'sinon';
import { v4 } from 'uuid';

import { advanceTime } from '../../../tests/helpers';
import { mockNetwork } from '../../../tests/mock-network';
Expand Down Expand Up @@ -64,7 +67,46 @@ type UnrestrictedControllerMessenger = ControllerMessenger<
| TransactionControllerEvents
>;

const createMockInternalAccount = ({
id = v4(),
address = '0x2990079bcdee240329a520d2444386fc119da21a',
name = 'Account 1',
importTime = Date.now(),
lastSelected = Date.now(),
}: {
id?: string;
address?: string;
name?: string;
importTime?: number;
lastSelected?: number;
} = {}): InternalAccount => {
return {
id,
address,
options: {},
methods: [
EthMethod.PersonalSign,
EthMethod.Sign,
EthMethod.SignTransaction,
EthMethod.SignTypedDataV1,
EthMethod.SignTypedDataV3,
EthMethod.SignTypedDataV4,
],
type: EthAccountType.Eoa,
metadata: {
name,
keyring: { type: 'HD Key Tree' },
importTime,
lastSelected,
},
} as InternalAccount;
};

const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207';
const INTERNAL_ACCOUNT_MOCK = createMockInternalAccount({
address: ACCOUNT_MOCK,
});

const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211';
const ACCOUNT_3_MOCK = '0xe688b84b23f322a994a53dbf8e15fa82cdb71127';
const infuraProjectId = 'fake-infura-project-id';
Expand Down Expand Up @@ -167,7 +209,8 @@ const setupController = async (
getNetworkClientRegistry:
networkController.getNetworkClientRegistry.bind(networkController),
getPermittedAccounts: async () => [ACCOUNT_MOCK],
getSelectedAddress: () => '0xdeadbeef',
getSelectedAccount: () =>
createMockInternalAccount({ address: '0xdeadbeef' }),
hooks: {},
isMultichainEnabled: false,
messenger,
Expand Down Expand Up @@ -802,7 +845,7 @@ describe('TransactionController Integration', () => {
await setupController({
isMultichainEnabled: true,
getPermittedAccounts: async () => [ACCOUNT_MOCK],
getSelectedAddress: () => ACCOUNT_MOCK,
getSelectedAccount: () => INTERNAL_ACCOUNT_MOCK,
});
const otherNetworkClientIdOnGoerli =
await networkController.upsertNetworkConfiguration(
Expand Down Expand Up @@ -883,7 +926,7 @@ describe('TransactionController Integration', () => {
await setupController({
isMultichainEnabled: true,
getPermittedAccounts: async () => [ACCOUNT_MOCK],
getSelectedAddress: () => ACCOUNT_MOCK,
getSelectedAccount: () => INTERNAL_ACCOUNT_MOCK,
});

const addTx1 = await transactionController.addTransaction(
Expand Down Expand Up @@ -1140,10 +1183,13 @@ describe('TransactionController Integration', () => {
});

const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to;
const selectedAccountMock = createMockInternalAccount({
address: selectedAddress,
});

const { networkController, transactionController } =
await setupController({
getSelectedAddress: () => selectedAddress,
getSelectedAccount: () => selectedAccountMock,
isMultichainEnabled: true,
});

Expand Down Expand Up @@ -1209,6 +1255,9 @@ describe('TransactionController Integration', () => {

it('should start the global incoming transaction helper when no networkClientIds provided', async () => {
const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to;
const selectedAccountMock = createMockInternalAccount({
address: selectedAddress,
});

mockNetwork({
networkClientConfiguration: buildInfuraNetworkClientConfiguration(
Expand All @@ -1226,7 +1275,7 @@ describe('TransactionController Integration', () => {
.reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK);

const { transactionController } = await setupController({
getSelectedAddress: () => selectedAddress,
getSelectedAccount: () => selectedAccountMock,
});

transactionController.startIncomingTransactionPolling();
Expand Down Expand Up @@ -1314,10 +1363,13 @@ describe('TransactionController Integration', () => {
});

const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to;
const selectedAccountMock = createMockInternalAccount({
address: selectedAddress,
});

const { networkController, transactionController } =
await setupController({
getSelectedAddress: () => selectedAddress,
getSelectedAccount: () => selectedAccountMock,
isMultichainEnabled: true,
});

Expand Down Expand Up @@ -1410,10 +1462,13 @@ describe('TransactionController Integration', () => {
describe('stopIncomingTransactionPolling', () => {
it('should not poll for new incoming transactions for the given networkClientId', async () => {
const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to;
const selectedAccountMock = createMockInternalAccount({
address: selectedAddress,
});

const { networkController, transactionController } =
await setupController({
getSelectedAddress: () => selectedAddress,
getSelectedAccount: () => selectedAccountMock,
});

const networkClients = networkController.getNetworkClientRegistry();
Expand Down Expand Up @@ -1454,9 +1509,12 @@ describe('TransactionController Integration', () => {

it('should stop the global incoming transaction helper when no networkClientIds provided', async () => {
const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to;
const selectedAccountMock = createMockInternalAccount({
address: selectedAddress,
});

const { transactionController } = await setupController({
getSelectedAddress: () => selectedAddress,
getSelectedAccount: () => selectedAccountMock,
});

mockNetwork({
Expand Down Expand Up @@ -1490,10 +1548,13 @@ describe('TransactionController Integration', () => {
describe('stopAllIncomingTransactionPolling', () => {
it('should not poll for incoming transactions on any network client', async () => {
const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to;
const selectedAccountMock = createMockInternalAccount({
address: selectedAddress,
});

const { networkController, transactionController } =
await setupController({
getSelectedAddress: () => selectedAddress,
getSelectedAccount: () => selectedAccountMock,
});

const networkClients = networkController.getNetworkClientRegistry();
Expand Down Expand Up @@ -1534,10 +1595,13 @@ describe('TransactionController Integration', () => {
describe('updateIncomingTransactions', () => {
it('should add incoming transactions to state with the correct chainId for the given networkClientId without waiting for the next block', async () => {
const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to;
const selectedAccountMock = createMockInternalAccount({
address: selectedAddress,
});

const { networkController, transactionController } =
await setupController({
getSelectedAddress: () => selectedAddress,
getSelectedAccount: () => selectedAccountMock,
isMultichainEnabled: true,
});

Expand Down Expand Up @@ -1600,9 +1664,12 @@ describe('TransactionController Integration', () => {

it('should update the incoming transactions for the gloablly selected network when no networkClientIds provided', async () => {
const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to;
const selectedAccountMock = createMockInternalAccount({
address: selectedAddress,
});

const { transactionController } = await setupController({
getSelectedAddress: () => selectedAddress,
getSelectedAccount: () => selectedAccountMock,
});

mockNetwork({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,21 @@ const BLOCK_TRACKER_MOCK = {

const CONTROLLER_ARGS_MOCK = {
blockTracker: BLOCK_TRACKER_MOCK,
getCurrentAccount: () => ADDRESS_MOCK,
getCurrentAccount: () => {
return {
id: '58def058-d35f-49a1-a7ab-e2580565f6f5',
address: ADDRESS_MOCK,
type: 'eip155:eoa' as const,
options: {},
methods: [],
metadata: {
name: 'Account 1',
keyring: { type: 'HD Key Tree' },
importTime: 1631619180000,
lastSelected: 1631619180000,
},
};
},
getLastFetchedBlockNumbers: () => ({}),
getChainId: () => CHAIN_ID_MOCK,
remoteTransactionSource: {} as RemoteTransactionSource,
Expand Down Expand Up @@ -546,7 +560,8 @@ describe('IncomingTransactionHelper', () => {
remoteTransactionSource: createRemoteTransactionSourceMock([
TRANSACTION_MOCK_2,
]),
getCurrentAccount: () => undefined as unknown as string,
// @ts-expect-error testing undefined
getCurrentAccount: () => undefined,
});

const { blockNumberListener } = await emitBlockTrackerLatestEvent(
Expand Down
Loading
Loading